用 typescript 整一门编程语言(伪)

前言

如果让你来设计一门编程语言,你最先会考虑啥?开发范式是面向对象、函数式,还是多范式;类型系统是静态类型还是动态类型;是否支持泛型;是通过缩进还是通过大括号分割代码块;行尾是否需要分号分隔……

越往下想,就变得一发不可收拾。想的越多,这门语言就变得愈发复杂,渐渐没了信心把这门语言实现出来。

要不换一种思路,想想最基本的一门语言有些什么。

基础

作为一门语言,最基础的就是语句和表达式了。表达式可以求值,语句不能。

这俩可以作为基类,衍生出其他我们想要的东西。譬如函数调用 Call 可以作为表达式的子类,而赋值 Setter 可以作为语句的子类

// 表达式基类
export abstract class Expr {}

// 语句基类
export abstract class Statement{}
复制代码

空值

绝大多数语言都会用一个值表示啥也没有

null
复制代码

而它也是一个值,也是最简单的一个值

export class Nil extends Expr {}
复制代码

再建个函数方便构建

export const nil = () => new Nil()
复制代码

整数运算

计算嘛,首先想到的就是整数运算了

(1 + 2 ) * 3 / 4 - 5

如何表达这么一个表达式呢,数字 + 加/减/乘/除运算符 + 括号 三个类吗?

其实只需要俩,数字和二元运算两个类就足够了,括号可以通过二元运算的组合表达出来。

// 整数值
export class Int extends Expr {
    val:number
    constructor(val:number) {
        super()
        this.val = val
    }
}

// 二元运算
export class BinaryArith extends Expr {
    arith:string
    a:Expr
    b:Expr
    constructor(arith:string,a:Expr,b:Expr) {
        super()
        this.arith = arith
        this.a = a
        this.b = b
    }
}
复制代码

再定义几个构建函数方便快速构建整数运算表达式

export const int = (num: number) => new Int(num)
export const add = (a: Expr, b: Expr) => new BinaryArith('add', a, b)
export const sub = (a: Expr, b: Expr) => new BinaryArith('sub', a, b)
export const mul = (a: Expr, b: Expr) => new BinaryArith('mul', a, b)
export const div = (a: Expr, b: Expr) => new BinaryArith('div', a, b)
复制代码

于是一开始的段代码可以表述成这样

mul(
    div(
        mul(
            add(
                int(1), 
                int(2)),
            int(3)),
        int(4)),
    int(5)
)
复制代码

声明 / 取值 / 赋值

接下来最常用的就应该是变量的声明、取值、赋值了。

let a = 1;
a = a + 1;
复制代码

其中声明和赋值是语句,而取值显然是可以求值的表达式

// 声明
export class Define extends Statement{
    name:string
    defVal:Expr // 默认值 
    constructor(name:string,defVal:Expr) {
        super()
        this.name = name
        this.defVal = defVal
    }
}
// 取值
export class Getter extends Expr {
    name:string
    constructor(name:string) {
        super()
        this.name = name
    }
}
// 赋值
export class Setter extends Statement{
    name:string
    val:Expr
    constructor(name:string,val:Expr) {
        super()
        this.name = name
        this.val = val
    }
}
复制代码

同样,再定义几个构建函数

export const define = (name:string,def:Expr) => new Define(name,def)
export const get = (name:string) => new Getter(name)
export const set = (name:string,val:Expr) => new Setter(name,val)
复制代码

于是实例的代码就能表述为

[
    define('a', int(1)),
    set('a', add(get('a'), int(1)))
]
复制代码

函数定义与调用

接着就是照葫芦画瓢,把函数相关的结构写出来

let add_1 = function(a){
    return a + 1
}

add_1(1)
复制代码

对于函数,无非是两部分 —— 参数和函数体

// 函数
export class Func extends Expr {
    argu: Argu[]
    body: Statement[]

    constructor(argu: Argu[], body: Statement[]) {
        super()
        this.argu = argu
        this.body = body
    }
}
复制代码

函数体很简单,就是一个语句的列表,而函数参数,包含了函数名,但其实作为函数的一部分,算不上语句,也更谈不上表达式。

// 参数
export class Argu {
    name: string
    constructor(name: string) {
        this.name = name
    }
}
复制代码

而函数体内还有一种特殊的语句 Return , 其接受一个值作为这个函数的返回值

// 返回语句
export class Return extends Statement {
    val: Expr
    constructor(val: Expr) {
        super()
        this.val = val
    }
}
复制代码

函数调用 Call 中,值得注意的是其中的 fn 不一定是直接 Func, 还可以是 Getter 或者另一个 Call , 所以这先声明为 Expr ,动态执行的时候再去验证是否为函数罢。

// 函数调用
export class Call extends Expr {
    fn: Expr
    inputs: Expr[]

    constructor(fn: Expr, inputs: Expr[]) {
        super()
        this.fn = fn
        this.inputs = inputs
    }
}
复制代码

还是在整几个构造函数方便构建

export const func = (...argu: Argu[]) => (...body: Statement[]) => new Func(argu, body)
export const argu = (name: string) => new Argu(name)
export const retu = (val: Expr) => new Return(val)
export const call = (fn: Expr, inputs: Expr[] = []) => new Call(fn, inputs)
复制代码

总结

let add3 = function(a,b,c){
    let res = 0
    res  = res + a
    res  = res + b
    res  = res + c
    return res
}

add3(1,2,3)
复制代码

以上的函数的结构用我们定义好的结构如何表示呢?

[
    define('add3', func([argu('a'), argu('b'), argu('c')],
        define('res',int(0)),
        set('res',add(get('res'),get('a'))),
        set('res',add(get('res'),get('b'))),
        set('res',add(get('res'),get('c'))),
        retu(get('res'))
    )),
    call(get('add3'),[int(1),int(2),int(3)])
]
复制代码

显然,相比上面,下面的代码是在是太难看了。但两者是等价的。这意味着前者可以通过一个函数转换成我们需要的结构。

甚至,可以用 html 来表述我们的代码。

<define name="add3">
    <func>
        <argu name="a" />
        <argu name="b" />
        <argu name="c" />

        <var name="res">
            <int>0</int>
        </var>

        <set name='res'>
            <add>
                <get name="res" />
                <get name="a" />
            </add>
        </set>
        <set name='res'>
            <add>
                <get name="res" />
                <get name="b" />
            </add>
        </set>
         <set name='res'>
            <add>
                <get name="res" />
                <get name="c" />
            </add>
        </set>
        <return>
            <get name="res" />
        </return>
    </func>
</define>



<call>
    <get name="add3"/>
    <int>1</int>
    <int>2</int>
    <int>3</int>
</call>
复制代码

至于如何转换则是词法分析,语法分析的范畴,也可以用 Lex/Yacc 、 ANTLR 等工具自动生成。但不是这篇文章讨论的重点。

后记

这篇文章实际上是定义了一个抽象语法树(AST)的结构。仔细看的话,这个结构很是简陋。没有循环,没有判断,连图灵完备都谈不上,只能解决整数的四则混合运算问题。

anyway,这些将来都可以安排上。

下一篇文章,在将这个抽象语法树转换成目标机器码之前,先把 “目标机器” ——虚拟机,整出来。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享