Building a Typechecker from scratch

1. Type theory and Basic types

1. Introduction to Type theory and checking

类型检查位于解释器顶层





// src/EvaTC.js
/**
 * Typed Eva: static typechecker
 */
class EvaTC {
    /**
     * Infers and validates type of expression
     */
    tc(exp) {
        // -----------------------------------
        // Self-evaluating

        /**
         * Numbers: 10
         */
        if (this._isNumber(exp)) {
            return 'number'
        }
    }

    /**
     * Whether the expression is a number
     */
    _isNumber(exp) {
        return typeof exp === 'number'
    }
}

module.exports = EvaTC
// __test__/run.js
const assert = require('assert')

const EvaTC = require('../src/EvaTC')

const eva = new EvaTC()

assert.equal(eva.tc(1), 'number')

console.log('All assertions passed!')

2. Typing Numbers and Strings Testing

// __test__/self-eval-test.js
const {test} = require('./test-utils')
const Type = require('../src/Types')

module.exports = eva => {
    // numbers
    test(eva, 42, Type.number)
    // strings
    test(eva, '"hello"', Type.string)
}
// __test__/run.js
const EvaTC = require('../src/EvaTC')

const tests = [
    require('./self-eval-test'),
]

const eva = new EvaTC()

tests.forEach(test => test(eva))

console.log('All assertions passed!')
// __test__/test-utils.js
const assert = require('assert')

function exec(eva, exp) {
    return eva.tc(exp)
}

function test(eva, exp, expected) {
    const actual = exec(eva, exp)

    try {
        assert.strictEqual(actual.equals(expected), true)
    } catch (e) {
        console.log(`Expected ${expected} type for ${exp}, but got ${actual}.`)
        throw e
    }
}

module.exports = {
    exec, test
}
// src/Types.js
class Type {
    constructor(name) {
        this.name = name
    }

    /**
     * Returns name
     */
    getName() {
        return this.name
    }

    /**
     * String representation
     */
    toString() {
        return this.getName()
    }

    /**
     * Equals
     */
    equals(other) {
        return this.name === other.name
    }
}

/**
 * Number type
 */
Type.number = new Type('number')

/**
 * String type
 */
Type.string = new Type('string')

module.exports = Type
// src/EvaTC.js
const Type = require('./Types')

/**
 * Typed Eva: static typechecker
 */
class EvaTC {
    /**
     * Infers and validates type of expression
     */
    tc(exp) {
        // -----------------------------------
        // Self-evaluating

        /**
         * Numbers: 10
         */
        if (this._isNumber(exp)) {
            return Type.number
        }

        /**
         * Strings: 'hello'
         */
        if (this._isString(exp)) {
            return Type.string
        }
    }

    /**
     * Whether the expression is a number
     */
    _isNumber(exp) {
        return typeof exp === 'number'
    }

    /**
     * Whether the expression is a string
     */
    _isString(exp) {
        return typeof exp === 'string' && exp[0] === '"' && exp.slice(-1) === '"'
    }
}

module.exports = EvaTC

3. Math binary operations String concat

// __test__/math-test.js
const {test} = require('./test-utils')
const Type = require('../src/Types')

module.exports = eva => {
    // Math
    test(eva, ['+', 2, 3], Type.number)
    test(eva, ['-', 1, 3], Type.number)
    test(eva, ['*', 2, 4], Type.number)
    test(eva, ['/', 4, 2], Type.number)
    test(eva, ['%', 5, 6], Type.number)
    // fail(eva, ['%', 5, '"6"'], Type.number) // should throw

    // String concat
    test(eva, ['+', '"hello"', '" world"'], Type.string)
    // test(eva, ['-', '"hello"', '" world"'], Type.string) // should throw
}
// src/Types.js
class Type {
    // ...

    /**
     * Equals
     */
    equals(other) {
        return this.name === other.name
    }
}
// src/EvaTC.js
const Type = require('./Types')

/**
 * Typed Eva: static typechecker
 */
class EvaTC {
    /**
     * Infers and validates type of expression
     */
    tc(exp) {
        // ...

        // -----------------------------------
        // math operator
        if (this._isBinary(exp)) {
            return this._binary(exp)
        }
    }

    /**
     * Whether the expression is a number
     */
    _isBinary(exp) {
        return /^[+\-*/%]$/.test(exp[0])
    }

    /**
     * Binary operators
     */
    _binary(exp) {
        this._checkArity(exp, 2)

        const t1 = this.tc(exp[1])
        const t2 = this.tc(exp[2])

        const allowedTypes = this._getOperandTypesForOperator(exp[0])

        this._expectOperatorType(t1, allowedTypes, exp)
        this._expectOperatorType(t2, allowedTypes, exp)

        return this._expect(t2, t1, exp[2], exp)
    }

    /**
     * Returns allowed operand types for an operator
     */
    _getOperandTypesForOperator(operator) {
        switch (operator) {
            case '+':
                return [Type.string, Type.number]
            case '-':
                return [Type.number]
            case '*':
                return [Type.number]
            case '/':
                return [Type.number]
            case '%':
                return [Type.number]
            default:
                throw `Unknown operator: ${operator}`
        }
    }

    _expectOperatorType(type_, allowedTypes, exp) {
        if (!allowedTypes.some(t => t.equals(type_))) {
            throw `\nUnexpected type: ${type_} in ${exp}, allowed: ${allowedTypes}`
        }
    }

    /**
     * Expects a type
     */
    _expect(actualType, expectedType, value, exp) {
        if (!actualType.equals(expectedType)) {
            this._throw(actualType, expectedType, value, exp)
        }
        return actualType
    }

    /**
     * Throws type error.
     */
    _throw(actualType, expectedType, value, exp) {
        throw `Expected "${expectedType}" type for ${value} in ${exp}, but got "${actualType}" type.\n`
    }

    /**
     * Throws for number arguments
     */
    _checkArity(exp, arity) {
        if (exp.length - 1 !== arity) {
            throw `\nOperator '${exp[0]}' expects ${arity} operands, ${exp.length - 1} given in ${exp}.\n`
        }
    }

    // ...
}

module.exports = EvaTC

4. Variables and Typing Environment, Г

// __test__/variable-test.js
const {test} = require('./test-utils')
const Type = require('../src/Types')

module.exports = eva => {
    // variable declaration;
    test(eva, ['var', 'x', 10], Type.number)
    // Variable declaration with type check:
    test(eva, ['var', ['y', 'number'], 10], Type.number)
    test(eva, ['var', ['y', 'number'], 'x'], Type.number)
    // variable access
    test(eva, 'x', Type.number)
    // global variable
    test(eva, 'VERSION', Type.string)
}
// src/TypeEnvironment.js
/**
 * TypeEnvironment: mapping from names to types
 */
class TypeEnvironment {
    // ...

    /**
     * Returns the type of defined variable,or throws
     * if the variable is not defined.
     */
    lookup(name) {
        // TODO:parent env lookup!
        if (!this.record.hasOwnProperty(name)) {
            throw new ReferenceError(`Variable ${name} is not defined.`)
        }
        return this.record[name]
    }
}

module.exports = TypeEnvironment
// src/EvaTC.js
class EvaTC {
    /**
     * Creates an Eva instance with the global environment
     */
    constructor() {
        /**
         * Create the Global TypeEnvironment per Eva instance.
         */
        this.global = this._createGlobal()
    }

    /**
     * Infers and validates type of expression
     */
    tc(exp, env = this.global) {
        // ...

        // -----------------------------------
        // Variable declaration: (var x 10)
        // with typecheck: (var (x number) "foo") // error
        if (exp[0] === 'var') {
            const [_tag, name, value] = exp

            // Infer actual type
            const valueType = this.tc(value, env)

            // with type check (var (x number) "foo")
            if (Array.isArray(name)) {
                const [varName, typeStr] = name

                const expectedType = Type.fromString(typeStr)

                // check the type
                this._expect(valueType, expectedType, value, exp)

                return env.define(varName, expectedType)
            }

            // simple name
            return env.define(name, valueType)
        }

        // -----------------------------------
        // Variable access:foo
        if (this._isVariableName(exp)) {
            return env.lookup(exp)
        }

        throw `Unknown type for expression ${exp}`
    }

    /**
     * Binary operators
     */
    _binary(exp, env) {
        this._checkArity(exp, 2)

        const t1 = this.tc(exp[1], env)
        const t2 = this.tc(exp[2], env)

        // ...
    }

    /**
     * Creates a Global TypeEnvironment.
     */
    _createGlobal() {
        return new TypeEnvironment({
            VERSION: Type.string
        });
    }

    /**
     * Whether the expression is a variable name.
     */
    _isVariableName(exp) {
        return typeof exp === 'string' && /^[+\-*/<>=a-zA-Z0-9_:]+$/.test(exp);
    }
}
// src/Types.js
class Type {
    /**
     * From string: 'number' -> Type.number
     */
    static fromString(typeStr) {
        if (this.hasOwnProperty(typeStr)) {
            return this[typeStr]
        }
        throw `Unknown type: ${typeStr}`
    }
}

5. Blocks and Local scope

// __test__/block-test.js
const Type = require('../src/Types')
const {test} = require('./test-utils')

module.exports = eva => {
    // Block: sequence of expressions.
    test(eva,
        ['begin',
            ['var', 'x', 10],
            ['var', 'y', 20],
            ['+', ['*', 'x', 10], 'y']
        ], Type.number)
    // Block: local scope
    test(eva,
        ['begin',
            ['var', 'x', 10],

            ['begin',
                ['var', 'x', '"hello"'],
                ['+', 'x', '"world"']
            ],

            ['-', 'x', 5]
        ], Type.number)
    // Block: access parent scopes
    test(eva,
        ['begin',
            ['var', 'x', 10],

            ['begin',
                ['var', 'y', 20],
                ['+', 'x', 'y']
            ],

            ['-', 'x', 5]
        ], Type.number)
    // Block: variable update
    test(eva,
        ['begin',
            ['var', 'x', 10],

            ['begin',
                ['var', 'y', 20],
                ['set', 'x', ['+', 'x', 'y']]
                // ['set', 'x', '"e"']
            ],
        ], Type.number)
}
// src/TypeEnvironment.js
/**
 * TypeEnvironment: mapping from names to types
 */
class TypeEnvironment {
    // ...

    /**
     * Returns the type of defined variable,or throws
     * if the variable is not defined.
     */
    lookup(name) {
        return this.resolve(name).record[name]
    }

    /**
     * Returns specific environment in which a variable is defined,or
     * throws if a variable is not defined.
     */
    resolve(name) {
        if (this.record.hasOwnProperty(name)) {
            return this
        }

        if (this.parent == null) {
            throw new ReferenceError(`Variable ${name} is not defined.`)
        }

        return this.parent.resolve(name)
    }
}
// src/EvaTC.js
const Type = require('./Types')
const TypeEnvironment = require('./TypeEnvironment')

/**
 * Typed Eva: static typechecker
 */
class EvaTC {
    // ...

    /**
     * Infers and validates type of expression
     */
    tc(exp, env = this.global) {

        // -----------------------------------
        // Variable update:(set x 10)
        if (exp[0] === 'set') {
            const [_, ref, value] = exp

            // The type of the new value should match to the
            // previous type when the variable was defined:
            const valueType = this.tc(value, env)
            const varType = this.tc(ref, env)

            return this._expect(valueType, varType, value, exp)
        }

        // -----------------------------------
        // Block:sequence of expressions
        if (exp[0] === 'begin') {
            const blockEnv = new TypeEnvironment({}, env)
            return this._tcBlock(exp, blockEnv)
        }

        throw `Unknown type for expression ${exp}`
    }

    /**
     * Checks a block
     */
    _tcBlock(exp, env) {
        let result

        const [_tag, ...expressions] = exp

        expressions.forEach(exp => {
            result = this.tc(exp, env)
        })

        return result;
    }
}

6. Parsing S-expression to AST

// eva-grammar.bnf
/**
   S-expression parser

   Atom: 42, foo, bar, "Hello"

   List: (), (+ 5 x), (print "hello")
 */

// -----------------------------
// Lexical grammar (tokens):

%lex

%%

\/\/.*                  /* skip comments */
\/\*[\s\S]*?\*\/        /* skip comments */

\s+                     /* skip whitespace */

\"[^\"]*\"              return 'STRING'

\d+                     return 'NUMBER'

[\w\-+*=<>/:,!]+         return 'SYMBOL'

/lex

// ------------------------------
// Syntactic grammer (BNF):

%%

Exp
    : Atom
    | List
    ;

// 最简单的表达式, 是数字 字符串和符号
Atom
    : NUMBER { $$ = Number($1) } // 转为实际的数字
    | STRING
    | SYMBOL
    ;

// 复杂表达式
List
    // $$ 表示返回值
    // 返回$2 - ListEntries
    : '(' ListEntries ')' { $$ = $2 }
    ;

// (Exp Exp Exp ...)

// ListEntries Exp
// ListEntries Exp Exp
// ListEntries Exp Exp Exp ...

ListEntries
    : ListEntries Exp { $1.push($2); $$ = $1 }
    | /* empty */ { $$ = [] }
    ;
// __test__/block-test.js
const Type = require('../src/Types')
const {test, exec} = require('./test-utils')

module.exports = eva => {
    // ...
    test(eva, `
        (begin
            (var x 10)
            (var y 20)
            (+ (* x 10) y)
        )
        `, Type.number)
    exec(eva, `(var x 10)`)
    test(eva, `
            (var y 20)
            (+ (* x 10) y)
        `, Type.number)
    // ...
}
// src/EvaTC.js
class EvaTC {
    tcGlobal(exp) {
        return this._tcBody(exp, this.global)
    }

    /**
     * Checks body (global or function).
     */
    _tcBody(body, env) {
        if (body[0] === 'begin') {
            return this._tcBlock(body, env)
        }
        return this.tc(body, env)
    }

    // ...
}
// __test__/test-utils.js
const assert = require('assert')
const evaParser = require('../parser/evaParser')

function exec(eva, exp) {
    if (typeof exp === 'string') {
        exp = evaParser.parse(`(begin ${exp})`)
    }
    return eva.tcGlobal(exp)
}

// ...

7. Control flow If and While expressions

// __test__/while-test.js
const Type = require('../src/Types')
const {exec, test} = require('./test-utils')

module.exports = eva => {
    test(eva, `
        (var x 10)

        (while (!= x 0)
            // (set x (- x 1))
            // x++ -> 一直循环
            // 依然测试通过, 因为类型检查器不关心运行时语义
            (set x (+ x 1))
        )

        x
    `, Type.number)
}
// __test__/if-test.js
const Type = require('../src/Types')
const {exec, test} = require('./test-utils')

module.exports = eva => {
    test(eva, `(<= x 10)`, Type.boolean)

    // If expression: both branches
    // should return the same type
    test(eva, `
        (var x 10)
        (var y 20)

        (if (<= x 10)
            (set y 1)
            (set y 2)
        )

        y
    `, Type.number)
}
// src/EvaTC.js
const Type = require('./Types')
const TypeEnvironment = require('./TypeEnvironment')

/**
 * Typed Eva: static typechecker
 */
class EvaTC {
    // ...

    tc(exp, env = this.global) {
        // -----------------------------------
        // Boolean: true | false
        if (this._isBoolean(exp)) {
            return Type.boolean
        }

        // -----------------------------------
        // boolean binary
        if (this._isBooleanBinary(exp)) {
            return this._booleanBinary(exp, env)
        }

        // -----------------------------------
        // if-expression:
        // rFel bool r e2 :t r H e2 :t
        // rFif el e2 else e2 }:t
        // Both branches should return the same time t.
        if (exp[0] === 'if') {
            const [_tag, condition, consequent, alternate] = exp

            // Boolean condition
            const t1 = this.tc(condition, env)
            this._expect(t1, Type.boolean, condition, exp)

            const t2 = this.tc(consequent, env)
            const t3 = this.tc(alternate, env)

            // Same types for both branches
            return this._expect(t3, t2, exp, exp)
        }

        // -----------------------------------
        // while-expression
        if (exp[0] === 'while') {
            const [_tag, condition, body] = exp

            // boolean condition
            const t1 = this.tc(condition, env)
            this._expect(t1, Type.boolean, condition, exp)

            return this.tc(body, env)
        }

        throw `Unknown type for expression ${exp}`
    }

    /**
     * Whether the expression is a boolean.
     */
    _isBoolean(exp) {
        return typeof exp === 'boolean' || exp === 'true' || exp === 'false';
    }

    /**
     * Whether the expression is boolean binary.
     */
    _isBooleanBinary(exp) {
        return (
            exp[0] === '==' ||
            exp[0] === '!=' ||
            exp[0] === '>=' ||
            exp[0] === '<=' ||
            exp[0] === '>' ||
            exp[0] === '<'
        )
    }

    _booleanBinary(exp, env) {
        this._checkArity(exp, 2)

        const t1 = this.tc(exp[1], env)
        const t2 = this.tc(exp[2], env)

        this._expect(t2, t1, exp[2], exp)

        return Type.boolean
    }
}
// __test__/self-eval-test.js
const {test} = require('./test-utils')
const Type = require('../src/Types')

module.exports = eva => {
    // numbers
    test(eva, 42, Type.number)
    // strings
    test(eva, '"hello"', Type.string)
    // boolean
    test(eva, true, Type.boolean)
    test(eva, false, Type.boolean)
}
// src/Types.js
// ...

/**
 * Boolean type
 */
Type.boolean = new Type('boolean')

2. Functional programming

1. User-defined functions Local environments

// __test__/user-defined-function-test.js
const Type = require('../src/Types')
const {exec, test} = require('./test-utils')

module.exports = eva => {
    test(eva, `
        (def square ((x number)) -> number
            (* x x))

        (square 2)
        // (square "2")
    `, Type.number)
    // Complex body
    test(eva, `
        (def calc ((x number) (y number)) -> number
            (begin
                (var z 30)
                (+ (* x y) z)
            )
        )
        
        (calc 10 20)
    `, Type.number)
}
// src/EvaTC.js

class EvaTC {
    // ...
    tc(exp, env = this.global) {
        // ...
        // -----------------------------------
        // Function declaration:(def square ((x number))->number (xx))
        if (exp[0] === 'def') {
            const [_tag, name, params, _retDel, returnTypeStr, body] = exp

            return env.define(
                name,
                this._tcFunction(params, returnTypeStr, body, env)
            )
        }

        // -----------------------------------
        // Function calls
        // (square 2)
        if (Array.isArray(exp)) {
            const fn = this.tc(exp[0], env)
            const argValues = exp.slice(1)

            // Passed arguments
            const argTypes = argValues.map(arg => this.tc(arg, env))

            return this._checkFunctionCall(fn, argTypes, env, exp)
        }

        throw `Unknown type for expression ${exp}`
    }

    /**
     * Checks function body.
     */
    _tcFunction(params, returnTypeStr, body, env) {
        const returnType = Type.fromString(returnTypeStr)

        // Parameters environment and types:
        const paramsRecord = {}
        const paramTypes = []

        params.forEach(([name, typeStr]) => {
            const paramType = Type.fromString(typeStr)
            paramsRecord[name] = paramType
            paramTypes.push(paramType)
        })

        const fnEnv = new TypeEnvironment(paramsRecord, env)

        // Check the body in the extended environment:
        const actualReturnType = this._tcBody(body, fnEnv)

        // Check return type
        if (!returnType.equals(actualReturnType)) {
            throw `Expected function ${body} to return ${returnType}, but got ${actualReturnType}.`
        }

        // Function type records its parameters and return type,
        // so we can use them to validate function calls:\
        return new Type.Function({
            paramTypes, returnType
        })
    }

    /**
     * Checks function call.
     */
    _checkFunctionCall(fn, argTypes, env, exp) {
        // Check arity
        if (fn.paramTypes.length !== argTypes.length) {
            throw `\nFunction ${exp[0]} ${fn.getName()} expects ${
                fn.paramTypes.length
            } arguments, ${argTypes.length} given in ${exp}.\n`
        }

        // Check if argument types match the parameter types:
        argTypes.forEach((argType, index) => {
            this._expect(argType, fn.paramTypes[index], argTypes[index], exp)
        })

        return fn.returnType
    }
}
// src/Types.js
class Type {
    // ...
    /**
     * From string: 'number' -> Type.number
     */
    static fromString(typeStr) {
        if (this.hasOwnProperty(typeStr)) {
            return this[typeStr]
        }

        // Functions
        if (typeStr.includes('Fn<')) {
            return Type.Function.fromString(typeStr)
        }

        throw `Unknown type: ${typeStr}`
    }
}

/**
 * Function meta type
 */
Type.Function = class extends Type {
    constructor({name = null, paramTypes, returnType}) {
        super(name);
        this.paramTypes = paramTypes
        this.returnType = returnType
        this.name = this.getName()
    }

    /**
     * Returns name:Fn<returnType<p1,p2,...>>
     *
     * Fn<number> - function which returns a number
     * Fn<number<number,number>> - function which returns a number,and accepts two number
     */
    getName() {
        if (this.name == null) {
            const name = ['Fn<', this.returnType.getName()]
            // params
            if (this.paramTypes.length !== 0) {
                const params = []
                for (let i = 0; i < this.paramTypes.length; i++) {
                    params.push(this.paramTypes[i].getName())
                }
                name.push('<', params.join(','), '>')
            }
            name.push('>')
            this.name = name.join('')
        }
        return this.name
    }

    /**
     * Equals
     */
    equals(other) {
        if (this.paramTypes.length !== other.paramTypes.length) {
            return false
        }

        // Same params
        for (let i = 0; i < this.paramTypes.length; i++) {
            if (!this.paramTypes[i].equals(other.paramTypes[i])) {
                return false
            }
        }

        // Return type
        if (!this.returnType.equals(other.returnType)) {
            return false
        }

        return true
    }

    /**
     * From string: 'Fn<number>' -> Type.Function
     */
    static fromString(typeStr) {
        // Already compiled
        if (Type.hasOwnProperty(typeStr)) {
            return Type[typeStr]
        }

        // Function type with return and params:
        let matched = /^Fn<(\w+)<([a-z,\s]+)>>$/.exec(typeStr)

        if (matched != null) {
            const [_, returnTypeStr, paramsString] = matched

            // Param types
            const paramTypes = paramsString
                .split(/,\s*/g)
                .map(param => Type.fromString(param))

            return (Type[typeStr] = new Type.Function({
                name: typeStr,
                paramTypes,
                returnType: Type.fromString(returnTypeStr)
            }))

            // Function type with return type only:
            matched = /^Fn<(\w+)>$/.exec(typeStr);

            if (matched != null) {
                const [_, returnTypeStr] = matched

                return (Type[typeStr] = new Type.Function({
                    name: typeStr,
                    paramTypes: [],
                    returnType: Type.fromString(returnTypeStr)
                }))
            }

            throw `Type.Function.fromString: Unknown type: ${typeStr}.`
        }
    }
}

2. Function calls Built-in functions

// src/EvaTC.js
// ...
_createGlobal()
{
    return new TypeEnvironment({
        VERSION: Type.string,

        sum: Type.fromString("Fn<number<number,number>>"),
        square: Type.fromString("Fn<number<number>>"),
    });
}
// __test__/built-in-function-test.js
const {test} = require('./test-utils')
const Type = require('../src/Types')

module.exports = eva => {
    test(eva, `(sum 1 5)`, Type.number)
    test(eva, `(sum (square 2) 4)`, Type.number)
}

3. Closures Recursive calls

// src/EvaTC.js
// ...
if (exp[0] === 'def') {
    const [_tag, name, params, _retDel, returnTypeStr, body] = exp

    // We have to extend environment with the function name *before*
    // evaluating the body -- this is need to support recursive
    // function calls:
    const paramTypes = params.map(([name, typeStr]) =>
        Type.fromString(typeStr)
    )

    // Predefine from the signature:
    env.define(
        name,
        new Type.Function({
            paramTypes,
            returnType: Type.fromString(returnTypeStr)
        })
    )

    // return env.define(
    //     name,
    //     this._tcFunction(params, returnTypeStr, body, env)
    // )

    // Actually validate the body
    return this._tcFunction(params, returnTypeStr, body, env)
}
// __test__/user-defined-function-test.js
const Type = require('../src/Types')
const {exec, test} = require('./test-utils')

module.exports = eva => {
    // ...
    // Closure
    test(eva, `
        (var value 100)
        
        (def calc ((x number) (y number)) -> Fn<number<number>>
            (begin
                (var z (+ x y))
                
                (def inner ((foo number)) -> number
                    (+ (+ foo z) value)
                )
                
                inner
            )
        )
        
        (var fn (calc 10 20))
        
        (fn 30)
    `, Type.number)
    // Recursive function
    test(eva, `
        (def factorial ((x number)) -> number
            (if (== x 1)
                1
                (* x (factorial (- x 1)))
            )
        )
        
        (factorial 5)
    `, Type.number)
}

4. Lambda functions and IILE Syntactic sugar

// src/EvaTC.js
class EvaTC {
    tc(exp, env = this.global) {

        // -----------------------------------
        // Function declaration:(def square ((x number))->number (xx))
        //
        // Syntactic sugar for:(var square (Lambda ((x number))->number (xx)))
        if (exp[0] === 'def') {
            // const [_tag, name, params, _retDel, returnTypeStr, body] = exp

            // Transpile to a variable declaration
            const varExp = this._transformDefToVarLambda(exp)

            const name = exp[1]
            const params = exp[2]
            const returnTypeStr = exp[4]

            // We have to extend environment with the function name *before*
            // evaluating the body -- this is need to support recursive
            // function calls:
            const paramTypes = params.map(([name, typeStr]) =>
                Type.fromString(typeStr)
            )

            // Predefine from the signature:
            env.define(
                name,
                new Type.Function({
                    paramTypes,
                    returnType: Type.fromString(returnTypeStr)
                })
            )

            // return env.define(
            //     name,
            //     this._tcFunction(params, returnTypeStr, body, env)
            // )

            // Actually validate the body
            // return this._tcFunction(params, returnTypeStr, body, env)
            return this.tc(varExp, env)
        }

        // -----------------------------------
        // Lambda function:(Lambda ((x number))->number (xx))
        if (exp[0] === 'lambda') {
            const [_tag, params, _retDel, returnTypeStr, body] = exp
            return this._tcFunction(params, returnTypeStr, body, env)
        }

        // ...
        throw `Unknown type for expression ${exp}`
    }

    // ...
    /**
     * Transforms def to var-lambda.
     */
    _transformDefToVarLambda(exp) {
        const [_tag, name, params, _retDel, returnTypeStr, body] = exp

        return ['var', name, ['lambda', params, _retDel, returnTypeStr, body]]
    }
}
// __test__/lambda-function-test.js
const Type = require('../src/Types')
const {exec, test} = require('./test-utils')

module.exports = eva => {
    // lambda function
    test(eva, `
        (lambda ((x number)) -> number (* x x))
    `, Type.fromString('Fn<number<number>>'))
    // Pass lambda as a callback
    test(eva, `
        (def onClick ((callback Fn<number<number>>)) -> number
            (begin
                (var x 10)     
                (var y 20)
                (callback (+ x y))
            )
        )
        
        (onClick (lambda ((data number)) -> number (* data 10)))
    `, Type.number)
    // Immediately-invoked lambda expression IILE:
    test(eva, `
        ((lambda ((x number)) -> number (* x x)) 2)
    `, Type.number)
    // Save lambda to a variable:
    test(eva, `
        (var square (lambda ((x number)) -> number (* x x)))
        (square 2)
    `, Type.number)
}

3. Type declarations and Classes

1. Declaring new types Type aliases

// src/EvaTC.js
class EvaTC {
    tc(exp, env = this.global) {
        // ...
        // -----------------------------------
        // Type declaration/alias:(type <namew><base>)
        if (exp[0] === 'type') {
            const [_tag, name, base] = exp

            // Type alias
            if (Type.hasOwnProperty(name)) {
                throw `Type ${name} is already defined: ${Type[name]}`
            }

            if (!Type.hasOwnProperty(base)) {
                throw `Type ${base} is not defined.`
            }

            return (Type[name] = new Type.Alias({
                name,
                parent: Type[base]
            }))
        }
        throw `Unknown type for expression ${exp}`
    }

    // ...
}
// __test__/alias-test.js
const {test, exec} = require('./test-utils')
const Type = require('../src/Types')

module.exports = eva => {
    exec(eva, `
        (type int number)
        (type ID int)
        (type Index ID)
    `)
    test(eva, `
        (def square ((x int)) -> int (* x x))
        (square 2)
    `, Type.int)
    test(eva, `
        (def promote ((userID ID)) -> ID (+ 1 userID))
        (promote 1)
    `, Type.ID)
    test(eva, `
        (var (x Index) 1)
    `, Type.Index)
    test(eva, `x`, Type.ID)
    test(eva, `x`, Type.int)
}
// src/Types.js
class Type {
    // ...
    equals(other) {
        if (other instanceof Type.Alias) {
            return other.equals(this)
        }

        return this.name === other.name
    }
}

/**
 * Type alias
 */
Type.Alias = class extends Type {
    constructor({name, parent}) {
        super(name);
        this.parent = parent
    }

    /**
     * Equals
     */
    equals(other) {
        if (this.name === other.name) {
            return true
        }
        return this.parent.equals(other)
    }

}

2. OOP Classes & 3. OOP Instances

// __test__/class-test.js
const {test} = require('./test-utils');
const Type = require('../src/Types');

module.exports = eva => {

    test(eva,
        `
    (class Point null
      (begin

        (var (x number) 0)
        (var (y number) 0)

        (def constructor ((self Point) (x number) (y number)) -> Point
          (begin
            (set (prop self x) x)
            (set (prop self y) y)
            self))

        (def calc ((self Point)) -> number
          (+ (prop self x) (prop self y)))))

    (var (p Point) (new Point 10 20))

    ((prop p calc) p)

  `,
        Type.number);
};
class EvaTC {
    // ...
    tc(exp, env = this.global) {
        // ...
        if (exp[0] === 'set') {
            const [_, ref, value] = exp

            // 1.Assignment to a property:
            if (ref[0] === 'prop') {
                const [_tag, instance, propName] = ref
                const instanceType = this.tc(instance, env)

                const valueType = this.tc(value, env)
                const propType = instanceType.getField(propName)

                return this._expect(valueType, propType, value, exp)
            }

            // ...
        }

        // -----------------------------------
        // Class declaration: (class <Name> <Super> <Body>)
        if (exp[0] === 'class') {
            const [_tag, name, superClassName, body] = exp

            // Resolve super
            const superClass = Type[superClassName]

            // New class (type)
            const classType = new Type.Class({name, superClass})

            // class is accessible by name.
            Type[name] = env.define(name, classType)

            // Body is evaluated in the class environment.
            this._tcBody(body, classType.env)

            return classType
        }

        // -----------------------------------
        // class instantiation:(new <class><Arguments>...)
        if (exp[0] === 'new') {
            const [_tag, className, ...argValues] = exp;
            const classType = Type[className];
            if (classType == null) {
                throw `Unknown class ${name}.`
            }
            const argTypes = argValues.map(arg => this.tc(arg, env))
            return this._checkFunctionCall(
                classType.getField('constructor'),
                [classType, ...argTypes],
                env, exp
            )
        }

        // -----------------------------------
        // Property access:(prop <instance><name>
        if (exp[0] === 'prop') {
            const [_tag, instance, name] = exp
            const instanceType = this.tc(instance, env)
            return instanceType.getField(name)
        }
        throw `Unknown type for expression ${exp}`
    }
}
// src/Types.js
// ...
/**
 * Null type
 */
Type.null = new Type('null')

/**
 * Class type:(class ...
 * Creates a new TypeEnvironment.
 */
Type.Class = class extends Type {
    constructor({name, superClass = Type.null}) {
        super(name)
        this.superClass = superClass
        this.env = new TypeEnvironment({}, superClass != Type.null ? superClass.env : null)
    }

    /**
     * Returns field type
     */
    getField(name) {
        return this.env.lookup(name)
    }

    /**
     * Equals
     */
    equals(other) {
        if (this === other) {
            return true
        }

        // Aliases
        if (other instanceof Type.Alias) {
            return other.equals(this)
        }

        // Super class
        if (this.superClass != Type.null) {
            // type: 子类 == 父类
            return this.superClass.equals(other)
        }

        // Anything else
        return false
    }
}

4. Super calls Inheritance

// src/EvaTC.js 

// -----------------------------------
// Super expressions:(super <ClassName>)
if (exp[0] === 'super') {
    const [_tag, className] = exp

    const classType = Type[className]

    if (classType == null) {
        throw `Unknown class ${name}.`
    }

    return classType.superClass
}
// __test__/class-test.js
const {test} = require('./test-utils');
const Type = require('../src/Types');

module.exports = eva => {

    // ...

    test(eva,
        `
    (class Point3D Point
      (begin

        (var (z number) 0)

        (def constructor ((self Point3D) (x number) (y number) (z number)) -> Point3D
          (begin
            ((prop (super Point3D) constructor) self x y)
            (set (prop self z) z)
            self))

        (def calc ((self Point3D)) -> number
          (+ ((prop (super Point3D) calc) self)
             (prop self z)))))

    (var (p Point3D) (new Point3D 10 20 30))

    ((prop p calc) p)

  `,
        Type.number);
};

4. Generic programming

1. Union type

// src/EvaTC.js

if (exp[0] === 'type') {
    const [_tag, name, base] = exp

    // Union type (e.g. (or number string))
    if (base[0] === 'or') {
        const options = base.slice(1)
        const optionTypes = options.map(option => Type.fromString(option))
        return (Type[name] = new Type.Union({name, optionTypes}))
    }

    // Type alias
    else {
        if (Type.hasOwnProperty(name)) {
            throw `Type ${name} is already defined: ${Type[name]}`
        }

        if (!Type.hasOwnProperty(base)) {
            throw `Type ${base} is not defined.`
        }

        return (Type[name] = new Type.Alias({
            name,
            parent: Type[base]
        }))
    }
}
// src/Types.js 

class Type {
    // ...
    equals(other) {
        if (other instanceof Type.Alias) {
            return other.equals(this)
        }

        if (other instanceof Type.Union) {
            return other.equals(this)
        }

        return this.name === other.name
    }
}

// ...
/**
 * Union type:(or string number)
 */
Type.Union = class extends Type {
    constructor({name, optionTypes}) {
        super(name)
        this.optionTypes = optionTypes
    }

    /**
     * Equals
     */
    equals(other) {
        if (this === other) {
            return true
        }

        // Aliases
        if (other instanceof Type.Alias) {
            return other.equals(this)
        }

        // Other union
        if (other instanceof Type.Union) {
            return this.includesAll(other.optionTypes)
        }

        // Anything else
        return this.optionTypes.some(t => t.equals(other))
    }

    /**
     * Whether this union includes all types.
     */
    includesAll(types) {
        if (types.length !== this.optionTypes.length) {
            return false
        }
        for (const type_ of types) {
            if (!this.equals(type_)) {
                return false
            }
        }
        return true
    }
}
// __test__/union-test.js
const {exec, test} = require('./test-utils');
const Type = require('../src/Types');

module.exports = eva => {

    exec(eva,
        `
    (type value (or number string))

    (type ID (or string number))

  `);

    test(eva,
        `
    (var (x value) 10)

    x

  `,
        Type.value);

    test(eva,
        `
    x
  `,
        Type.number);

    test(eva,
        `
    x
  `,
        Type.string);

    test(eva,
        `
    (var (y value) "hello")

    y 
  `,
        Type.value);

    test(eva,
        ` 
    (var (a value) 10)
    (var (b ID) "x")

    (def process ((id ID)) -> number
      (+ a id))
      // (- "a" id))

    (process b)

    (+ a b) 
  `,
        Type.number);
}

2. Union Type narrowing

// src/EvaTC.js 

class EvaTC {
    tc(exp, env = this.global) {
        // ...
        if (exp[0] === 'if') {
            const [_tag, condition, consequent, alternate] = exp

            // Boolean condition
            const t1 = this.tc(condition, env)
            this._expect(t1, Type.boolean, condition, exp)

            // Initially,environment used to tc consequent part
            // is the same as the main env,however can be updated
            // for the union type with type casting:
            let consequentEnv = env

            // Check if the condition is a type casting rule.
            // This is used with union types to make a type concrete:
            // (if (== (typeof foo) "string")...)
            if (this._isTypeCastCondition(condition)) {
                const [name, specificType] = this._getSpecifiedType(condition)

                // Update environment with the concrete type for this name:
                consequentEnv = new TypeEnvironment(
                    {[name]: Type.fromString(specificType)},
                    env
                )
            }

            const t2 = this.tc(consequent, consequentEnv)
            const t3 = this.tc(alternate, env)

            // Same types for both branches
            return this._expect(t3, t2, exp, exp)
        }

        throw `Unknown type for expression ${exp}`
    }

    // ...

    _expectOperatorType(type_, allowedTypes, exp) {
        // For union type, _all_ sub-types should support this operation:
        if (type_ instanceof Type.Union) {
            if (type_.includesAll(allowedTypes)) {
                return
            }
        }

        // other types
        else {
            if (allowedTypes.some(t => t.equals(type_))) {
                return;
            }
        }

        throw `\nUnexpected type:${type_} in ${exp}, allowed: ${allowedTypes}.`
    }

    _createGlobal() {
        return new TypeEnvironment({
            VERSION: Type.string,

            sum: Type.fromString("Fn<number<number,number>>"),
            square: Type.fromString("Fn<number<number>>"),

            typeof: Type.fromString('Fn<string<any>>')
        });
    }

    _checkFunctionCall(fn, argTypes, env, exp) {
        // ...

        // Check if argument types match the parameter types:
        argTypes.forEach((argType, index) => {
            if (fn.paramTypes[index] === Type.any) {
                return
            }
            this._expect(argType, fn.paramTypes[index], argTypes[index], exp)
        })

        return fn.returnType
    }

    /**
     * Whether the if-condition is type casting/specification.
     *
     * This is used with union types to make a type concrete:
     *
     * (if (=(typeof foo)"string")...)
     */
    _isTypeCastCondition(condition) {
        const [op, lhs] = condition
        return op === '==' && lhs[0] === 'typeof'
    }

    /**
     * Returns specific type after casting.
     *
     * This is used with union types to make a type concrete:
     *
     * (if (=(typeof foo)"string")...)
     */
    _getSpecifiedType(condition) {
        const [op, [_typeof, name], specificType] = condition;
        // Return name and the new type (stripping quotes).
        return [name, specificType.slice(1, -1)];
    }
}
// src/Types.js
/**
 * Any type
 */
Type.any = new Type('any')
// __test__/union-test.js
const {exec, test} = require('./test-utils');
const Type = require('../src/Types');

module.exports = eva => {

    exec(eva,
        `
    (type value (or number string))

    (type ID (or string number))

  `);

    test(eva,
        `
    (var (x value) 10)

    x

  `,
        Type.value);

    test(eva,
        `
    x
  `,
        Type.number);

    test(eva,
        `
    x
  `,
        Type.string);

    test(eva,
        `
    (var (y value) "hello")

    y 
  `,
        Type.value);

    test(eva,
        ` 
    (var (a value) 10)
    (var (b ID) "x")

    (def process ((id ID)) -> number
      (+ a id))
      // (- a id)) // should panic(cause string can't use - operator)
      // (- "a" id))

    (process b)

    (+ a b) 
  `,
        Type.number);

    test(eva,
        `
    (def accept ((x value)) -> value
      (begin
        (if (== (typeof x) "number")
            (- 1 x)
            (+ "y" x))))

    (accept 10)
  `,
        Type.number);

    test(eva,
        `
    (accept "x")
  `,
        Type.string);
}

3. Generics Function declarations

// __test__/generic-test.js
const {test, exec} = require('./test-utils');
const Type = require('../src/Types');

module.exports = eva => {
    // Generic function
    exec(eva, `
        // (def combine ((x number) (y number)) -> number (+ x y))
        // (def combine ((x string) (y string)) -> string (+ x y))
        
        (def combine <K> ((x K) (y K)) -> K (+ x y))
    `)
}
// src/EvaTC.js
class EvaTC {
    tc(exp, env = this.global) {

        if (exp[0] === 'def') {
            // const [_tag, name, params, _retDel, returnTypeStr, body] = exp

            // Transpile to a variable declaration
            const varExp = this._transformDefToVarLambda(exp)

            if (!this._isGenericDefFunction(exp)) {

                const name = exp[1]
                const params = exp[2]
                const returnTypeStr = exp[4]

                // We have to extend environment with the function name *before*
                // evaluating the body -- this is need to support recursive
                // function calls:
                const paramTypes = params.map(([name, typeStr]) =>
                    Type.fromString(typeStr)
                )

                // Predefine from the signature:
                env.define(
                    name,
                    new Type.Function({
                        paramTypes,
                        returnType: Type.fromString(returnTypeStr)
                    })
                )
            }

            // return env.define(
            //     name,
            //     this._tcFunction(params, returnTypeStr, body, env)
            // )

            // Actually validate the body
            // return this._tcFunction(params, returnTypeStr, body, env)
            return this.tc(varExp, env)
        }

        if (exp[0] === 'lambda') {
            // 1. Generic function
            if (this._isGenericLambdaFunction(exp)) {
                return this._createGenericFunctionType(exp, env)
            }

            // 2. Simple function
            return this._createSimpleFunctionType(exp, env)
        }
        throw `Unknown type for expression ${exp}`
    }

    /**
     * Generic function declarations.
     * Such functions are not checked at declaration,
     * instead they are checked at call time,when all
     * generic parameters are bound.
     */
    _createGenericFunctionType(exp, env) {
        const [_tag, genericTypes, params, _retDel, returnType, body] = exp

        return new Type.GenericFunction({
            genericTypesStr: genericTypes.slice(1, -1),
            params, body, returnType,
            env // Closure
        })
    }

    /**
     * Simple function declarations (no generic parameters).
     *
     * Such functions are type-checked during declaration time.
     */
    _createSimpleFunctionType(exp, env) {
        const [_tag, params, _retDel, returnTypeStr, body] = exp
        return this._tcFunction(params, returnTypeStr, body, env)
    }

    /**
     * Whether the function is generic.
     *
     * (def foo <K>((x K))->K (xx))
     */
    _isGenericDefFunction(exp) {
        return exp.length === 7 && /^<[^>]+>$/.test(exp[2])
    }

    /**
     * Whether the function is generic.
     *
     * (lambda <K>((x K))->K (xx))
     */
    _isGenericLambdaFunction(exp) {
        return exp.length === 6 && /^<[^>]+>$/.test(exp[1])
    }

    /**
     * Transforms def to var-lambda.
     */
    _transformDefToVarLambda(exp) {
        // 1. Generic function
        if (this._isGenericDefFunction(exp)) {
            const [_tag, name, genericTypesStr, params, _retDel, returnTypeStr, body] = exp;
            return ['var', name, ['lambda', genericTypesStr, params, _retDel, returnTypeStr, body]];
        }

        // 2. Simple function
        const [_tag, name, params, _retDel, returnTypeStr, body] = exp

        return ['var', name, ['lambda', params, _retDel, returnTypeStr, body]]
    }
} 
// src/Types.js
/**
 * Generic function type.
 * Generic functions create normal function types
 * when a function is called.
 */
Type.GenericFunction = class extends Type {
    constructor({name = null, genericTypesStr, params, body, returnType, env}) {
        super(`${name || 'lambda'} <${genericTypesStr}>`);
        this.genericTypes = genericTypesStr.split(',')
        this.params = params
        this.body = body
        this.returnType = returnType
        this.env = env
    }
}

4. Generics Function calls

// src/EvaTC.js

class EvaTC {
    tc(exp, env = this.global) {
        if (Array.isArray(exp)) {
            const fn = this.tc(exp[0], env)

            // Simple function calls
            let actualFn = fn
            let argValues = exp.slice(1)

            // Generic function call
            if (fn instanceof Type.GenericFunction) {
                // Actual (instantiated) types
                const actualTypes = this._extractActualCallTypes(exp)

                // Map the generic types to the actual types:
                const genericTypesMap = this._getGenericTypesMap(
                    fn.genericTypes,
                    actualTypes
                )

                // Bind parameters and return types:
                const [boundParams, boundReturnType] = this._bindFunctionTypes(
                    fn.params, fn.returnType,
                    genericTypesMap // {K: number}
                )

                // Check function body with the bound parameter types:
                // This creates an actual function type.

                // Notice:we pass environment as fn.env,i.e.closured environment
                // TODO: pre-install to Type combine_number,combine_string,etc.
                actualFn = this._tcFunction(
                    boundParams, boundReturnType,
                    fn.body,
                    fn.env // Closure
                )

                // In generic function calls parameters are passed from index 2:
                argValues = exp.slice(2) // (combine <number> 2 3)
            }

            // Passed arguments
            const argTypes = argValues.map(arg => this.tc(arg, env))

            return this._checkFunctionCall(actualFn, argTypes, env, exp)
        }

        throw `Unknown type for expression ${exp}`
    }

    /**
     * Maps generic parameter types to actual types.
     */
    _getGenericTypesMap(genericTypes, actualType) {
        const boundTypes = new Map()
        for (let i = 0; i < genericTypes.length; i++) {
            boundTypes.set(genericTypes[i], actualType[i])
        }
        return boundTypes
    }

    /**
     * Binds generic parameters and return type to actual types.
     */
    _bindFunctionTypes(params, returnType, genericTypesMap) {
        const actualParams = []

        // 1.Bind parameter types:
        for (let i = 0; i < params.length; i++) {
            const [paramName, paramType] = params[i]

            let actualParamType = paramType

            // Generic type -> rewrite to actual.
            // genericTypesMap({K: number})
            if (genericTypesMap.has(paramType)) {
                actualParamType = genericTypesMap.get(paramType)
            }

            actualParams.push([paramName, actualParamType])
        }

        // 2.Bind return type:
        let actualReturnType = returnType

        if (genericTypesMap.has(returnType)) {
            actualReturnType = genericTypesMap.get(returnType)
        }

        return [actualParams, actualReturnType]
    }

    /**
     * Extracts types for generic parameter types.
     * (combine <string> "hello")
     */
    _extractActualCallTypes(exp) {
        const data = /^<([^>]+)>$/.exec(exp[1])

        if (data == null) {
            throw `No actual types provided in generic call: ${exp}.`
        }

        return data[1].split(',')
    }
}
// __test__/generic-test.js
const {test, exec} = require('./test-utils');
const Type = require('../src/Types');

module.exports = eva => {
    // Generic function
    exec(eva, `
        // (def combine ((x number) (y number)) -> number (+ x y))
        // (def combine ((x string) (y string)) -> string (+ x y))
        
        (def combine <K> ((x K) (y K)) -> K (+ x y))
    `)
    test(eva, `
        (combine <number> 2 3)
    `, Type.number)
    test(eva, `
        (combine <string> "hello" " world")
    `, Type.string)
    test(eva, `
        ((lambda <K> ((x K)) -> K (+ x x)) <number> 2)
    `, Type.number)
    test(eva, `
        ((lambda <K> ((x K)) -> K (+ x x)) <string> "hello")
    `, Type.string)
}

5. Final executable

/**
 * Class declaration.
 */

(class Point null
  (begin

    (var (x number) 0)
    (var (y number) 0)

    (def constructor ((self Point) (x number) (y number)) -> Point
      (begin
        (set (prop self x) x)
        (set (prop self y) y)
        self))

    (def calc ((self Point)) -> number
      (+ (prop self x) (prop self y)))))

(var (p Point) (new Point 10 20))

((prop p calc) p)

/**
 * Child class.
 */

(class Point3D Point
  (begin

    (var (z number) 0)

    (def constructor ((self Point3D) (x number) (y number) (z number)) -> Point3D
      (begin
        ((prop (super Point3D) constructor) self x y)
        (set (prop self z) z)
        self))

    (def calc ((self Point3D)) -> number
      (+ ((prop (super Point3D) calc) self)
         (prop self z)))))

(var (p Point3D) (new Point3D 10 20 30))

((prop p calc) p)


/*

What's next?

1. Shapes (aka Objects, aka Interfaces)

(type User
  (object
    (name string)
    (age number)))

(var (user User)
  (object
    (name "John")
    (age 25)))

// (interface  ...) -> syntactic sugar for (type  (object ...))

(interface User
  (name string)
  (age number))

(class    )

2. Async functions

(async def fetch  )

3. Array literals:

(array ...)

4. Type: valueBelongs, isOperationSupported


5. Values union

(type names (or "alex" "john"))

6. Infer types for generic calls:

(combine  2 3) -> (combine 2 3)

7. etc
*/
#!/usr/bin/env node
// bin/eva-tc.js
'use strict'

const EvaTC = require("../src/EvaTC");
const evaParser = require("../parser/evaParser");
const fs = require("fs");

function tcGlobal(src, eva) {
    const exp = evaParser.parse(`(begin ${src})`)

    try {
        eva.tcGlobal(exp)
        console.log('No errors!')
    } catch (e) {
        console.error(e)
    }
}

/**
 * Examples:
 *
 * ./bin/eva-tc -e (2 3)'
 * ./bin/eva-tc -f ~/test.eva
 */
function main(argv) {
    const [_node, _path, mode, exp] = argv

    const eva = new EvaTC()

    // 1.Direct expression:
    if (mode === '-e') {
        return tcGlobal(exp, eva)
    }

    // 2.Eva file:
    if (mode === '-f') {
        const src = fs.readFileSync(exp, 'utf8')
        return tcGlobal(src, eva)
    }
}

main(process.argv)

  目录