前言
如果让你来设计一门编程语言,你最先会考虑啥?开发范式是面向对象、函数式,还是多范式;类型系统是静态类型还是动态类型;是否支持泛型;是通过缩进还是通过大括号分割代码块;行尾是否需要分号分隔……
越往下想,就变得一发不可收拾。想的越多,这门语言就变得愈发复杂,渐渐没了信心把这门语言实现出来。
要不换一种思路,想想最基本的一门语言有些什么。
基础
作为一门语言,最基础的就是语句和表达式了。表达式可以求值,语句不能。
这俩可以作为基类,衍生出其他我们想要的东西。譬如函数调用 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,这些将来都可以安排上。
下一篇文章,在将这个抽象语法树转换成目标机器码之前,先把 “目标机器” ——虚拟机,整出来。