lelang编程语言

版本0.1
lelang编程语言是一门静态类型检查,基于LLVM的系统级编程语言,可借助LLVM工具编译到任何被LLVM支持的目标机器上。
lelang的编译器使用Rust语言编写。
更新于 2022.4.24

编写该语言的动机

为什么要设计该语言

  • 因为一直都对编译技术比较感兴趣,在学习了一些编译技术后,就想动手实现一个较为完整的编程语言,并考虑作为长期维护项目。
  • 首先考虑实现一个脚本语言,在参考了Lua语言、Python语言、JavaScript语言后,觉得脚本语言虽然在后端实现上较为灵活,但是都会需要运行时环境,例如虚拟机。
  • 刚好可以作为一个课程实验进行

为什么设计成静态检查,基于LLVM的语言?

  • 由于是学习性质的项目,使用LLVM可以接触到更多的底层知识和优化手段,并且可以方便的直接编译到独立运行的可执行文件,并可使用LLVM工具链辅助完成调试,优化等工作,并且如果后续 需要支持解释执行,借用LLVM的JIT,或者在现有的基础上重写一个解释器作为后端也是十分方便的,所以lelang最终定位为了一个偏向进行更多的静态检查,支持直接编译到机器码,并使用可使用 标准C ABI和系统进行交互的语言。
  • 而现有的编程语言中,比较流行的类似的分别是C、C++、Go、Rust,lelang的设计对这几个语言均有参考。C语言虽然经典好用,但是语言提供的抽象能力太差,也没有提供任何的安全检查机制和垃圾收集机制,开发难度较大。 C++语言特性很多,功能强大,但是上手难度较高,历史包袱较重,使用难度也较高。Rust语言上手难度同样较高,继承和改进了很多C++的问题,但是编译检查十分严格,并且内容也非常多,且牵涉到许多C系语言中很少使用的概念,并且也需要手动管理内存。 最后Go语言虽然满足易用,静态检查,上手简单,自动垃圾回收等需求,但是Go语言某些部分的语法设计过于粗略,例如any类型,使用积类型而不是和类型进行错误处理,不支持任何可变性约束等等。
  • 所以lelang基于Go语言的优点,预计在其基础上增加例如代数数据类型,模式匹配,泛型等语法,基于LLVM的后端可方便支持跨各种操作系统和硬件平台的编译,引入自动垃圾收集器,摆脱手动管理内存的麻烦。

为什么使用Rust语言编写前端?

  • Rust语言是我个人比较喜欢的一门语言。但是平时C++,Java,Python等语言使用的更多,一直没有一个使用Rust实践一个完整项目的机会,于是考虑使用Rust

Rust语言支持多种函数式编程语言的语法和概念,但是又不像某些纯函数式语言(例如Haskell,Lisp语言)那样完全放弃命令式的语法,并且不需要任何解释器环境就可运行,其中的代数数据类型,模式匹配,和基于和类型的错误处理等特性都十分适合用来编写一个新语言的编译器。

  • Rust的工程化和易用性做的很好,不管是开发还是部署,均比C++要容易,并且严格的编译检查和方便的日志功能能很好的减少Debug的时间。

lelang编译器

本章将介绍如何安装lelang编译器,以及如何调用编译器,编译源代码文件为llvm ir,汇编代码,目标文件以及可执行文件,你将读到:

  • 在windows、linux、macos上如何安装lelang编译器
  • 将含有一个计算斐波那契数某一项的函数的源代码文件调用lelang编译器编译

安装与使用

1. 编译lelang编译器

  • Linux
    1. Linux下编译lelang compiler需要安装llvm-12和libclang
      可使用 debian系可使用 apt 进行安装(目前仅在ubuntu下测试),运行即可:
      apt install llvm-12 libclang
    
    1. 安装Rust
      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
    1. 编译项目
     cargo build --release
    
    1. 运行项目
    cargo run --release
    
  • macos
    1. 使用homebrew安装llvm12
       llvm@12
    
    1. 安装Rust
      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
    1. homebrew默认不会把llvm加入环境变量,需要设置LLVM_SYS_PREFIX120环境变量为llvm目录。
    2. 编译项目
     cargo build --release
    
    1. 运行项目
    cargo run --release
    
  • windows
    1. 编译llvm12
      由于llvm官方在windows下发布的预编译二进制包不含某些需要的静态库,以及llvm-config工具,所以需要自己编译(后续本项目会发布windows下的预编译编译器)
    2. 安装Rust
      使用rust-init安装
    3. 设置LLVM_SYS_PREFIX120环境变量为llvm目录
    4. 编译项目
     cargo build --release
    
    5运行项目
    cargo run --release
    

使用

运行 cargo build --release后,可在target/release/下找到lelang可执行文件,运行./lelang --help可打印以下帮助信息

lelang
lelang programming language compiler, based on LLVM compiler infrastructure

USAGE:
    lelang [OPTIONS] -i <SOURCE_FILE_PATH>

OPTIONS:
    -h, --help                   Print help information
    -i <SOURCE_FILE_PATH>        Set compiler source file path
    -o <OUTPUT_FILE_PATH>        Set compiler output path [default: ./a.out]
    -O <OPTIMIZE_LEVEL>          Set compiler optimize level [default: 0]
    -S <OUTPUT_FORMAT>           Set compiler output format [default: obj] [possible values: ir,
                                 asm, obj, exe]

打印斐波那契数

  1. 新建文件main.le
decl le print_int32(i32);

le fibonacci(step:i32)->i32{
    if(step3){
        ret 1;
    }el{
        ret fibonacci(step-1)+fibonacci(step-2);
    }
}

le main()->i32{
    print_int32(fibonacci(20));
    ret 0;
}
  1. 执行lelang -i main.le -S ir -o out.ll得到
; ModuleID = 'main'
source_filename = "main"
target triple = "arm64-apple-darwin21.4.0"

declare void @print_int32(i32)

define i32 @fibonacci(i32 %0) {
  %2 = alloca i32, align 4
  %3 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  %4 = load i32, i32* %3, align 4
  %5 = icmp slt i32 %4, 3
  br i1 %5, label %6, label %7

6:                                                ; preds = %1
  store i32 1, i32* %2, align 4
  br label %16

7:                                                ; preds = %1
  %8 = load i32, i32* %3, align 4
  %9 = sub i32 %8, 1
  %10 = call i32 @fibonacci(i32 %9)
  %11 = load i32, i32* %3, align 4
  %12 = sub i32 %11, 2
  %13 = call i32 @fibonacci(i32 %12)
  %14 = add i32 %10, %13
  store i32 %14, i32* %2, align 4
  br label %16

15:                                               ; No predecessors!
  br label %16

16:                                               ; preds = %15, %7, %6
  %17 = load i32, i32* %2, align 4
  ret i32 %17
}

define i32 @main() {
  %1 = alloca i32, align 4
  %2 = call i32 @fibonacci(i32 20)
  call void @print_int32(i32 %2)
  store i32 0, i32* %1, align 4
  br label %3

3:                                                ; preds = %0
  %4 = load i32, i32* %1, align 4
  ret i32 %4
}
  1. 执行lelang -i main.le -S exe -o out,并运行out,程序打印
6765
decl le print_int32(i32);

le fibonacci(step:i32)->i32{
    if(step3){
        ret 1;
    }el{
        ret fibonacci(step-1)+fibonacci(step-2);
    }
}

le main()->i32{
    print_int32(fibonacci(20));
    ret 0;
}

基本类型

本章你将了解lelang支持所有基本数据类型,它们包括:

  • 无符号与有符号整数类型各4种
  • 2种长度的浮点数
  • 结构体类型
  • 数组类型

整数与浮点数

与C或C++不同,lelang要求显示指定整数与浮点数的长度,lelang保证在任何实现lelang编译器的平台上均为同样的长度。

整数共有8种类型

  • i8~i32
  • u8~u32

浮点数共有2种类型

  • f32
  • f64

Tips: lelang禁止不同类型的整数或浮点数发生隐式转换,例如下面的代码无法通过编译

decl le print_int64(i64);


le main()->i32{
    var width_64:i32 = 100;
    print_int64(width_64);
    ret 0;
}

编译它,我们得到👉🏻

[E0014] Error: expect a type `i64`, but got `i32`
   ╭─[docs/src/test_sources/invalid_conversion.le:6:16]
   │
 6 │     print_int64(width_64);
   ·                ─────┬─────
   ·                     ╰─────── expect type `i64`, but found type `i32`
   ·
   · Help: maybe you need a type cast to type `i64` ?`
───╯

同样,不同类型的整数或浮点数也不能直接相加


le main()->i32{
    var width_32:i32 = 100;
    var width_64_float:f64 = 10.33;
    width_32=width_32+width_64_float;
    ret 0;
}

编译它,我们得到👉🏻

[E0012] Error: no suitable binary operator `+` for type: `i32` and `f64`
   ╭─[docs/src/test_sources/invalid_conversion1.le:5:14]
   │
 5 │     width_32=width_32+width_64_float;
   ·              ───────────┬───────────
   ·                         ╰───────────── operator `+` not suitable for type `i32` and type `f64` here
   ·
   · Help: maybe you need a `as` type cast here?
───╯

结构体

lelang的结构体与C语言类似,结构体成员可以是任意非本结构体类型。

## structure declare
struct MyStruct{
    member1:i32, ## i32 type member
    member2:f64, ## f64 type member
    member3:[i32;2] ## length 2 i32 array member
}

数组

lelang的数组为基本类型,不像C或C++中的数组通常需要退化为指针才可进行操作,并且会丢失长度信息。
lelang中同类型的数组可以直接进行比较或赋值,也可以直接作为函数的参数或者返回值,甚至,数组类型也可以有成员函数。 数组必须在定义时初始化其所有成员,例如:

函数

和许多其他系统级编程语言一样,函数是lelang运行程序的基本单位,一个lelang函数由四部分组成。

  • 函数声明关键字
  • 函数名字
  • 参数列表
  • 返回值

## le -->function define
## fibonacci -->function name 
## step -->parameter name
## i32 -->parameter type
## -->i32 -->return type
## {...} -->function body
le fibonacci(step:i32)->i32{   
    if(step<3){                
        ret 1;
    }el{
        ret fibonacci(step-1)+fibonacci(step-2);
    }
}

函数定义与调用

外部函数声明

分支

if语句

循环

for循环

while循环