終於實現了一門屬於自己的程式語言

2022-09-07 09:03:41

前言

都說程式設計師的三大浪漫是:作業系統、編譯原理、圖學;最後的圖學確實是特定的專業領域,我們幾乎接觸不到,所以對我來說換成網路更合適一些,最後再加上一個資料庫。

這四項技術如果都能掌握的話那豈不是在 IT 行業橫著走了,加上這幾年網際網路行業越來越不景氣,越底層的技術就越不可能被替代;所以為了給自己的 30+ 危機留點出路,從今年上半年開始我就逐漸開始從頭學習編譯原理。

功夫不負有心人,經過近一個月的挑燈夜戰,每晚都在老婆的催促下才休息,克服了中途好幾次想放棄的衝動,終於現在完成了 GScript 一個預覽版。

預覽版的意思是語法結構與整體設計基本完成,後續更新也不太會改動這部分內容、但還缺少一些易用功能。

特性

首先來看看保留環節, GScript 是如何編寫 hello world 的。

hello_world.gs:

println("hello world");
❯ gscript hello_world.gs
hello world

廢話說完了接下來重點聊聊 GScript 所支援的特性了。


後文會重點說明每一個特性。

例子

除了剛才提到的 hello world,再來看一個也是範例程式碼經常演示的列印斐波那契數列

void fib(){
    int a = 0;
    int b = 1;
    int fibonacci(){
        int c = a;
        a = b;
        b = a+c;
        return c;
    }
    return fibonacci;
}
func int() f = fib();
for (int i = 0; i < 5; i++){
    println(f());
}

輸出結果如下:

0
1
1
2
3

整體寫法與 Go 官方推薦的類似:https://go.dev/play/p/NeGuDahW2yP

// fib returns a function that returns
// successive Fibonacci numbers.
func fib() func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		return a
	}
}
func main() {
	f := fib()
	// Function calls are evaluated left-to-right.
	fmt.Println(f(), f(), f(), f(), f())
}

都是通過閉包變數實現的,同時也展示了 GScript 對閉包、函數的使用,後文詳細介紹閉包的用法。

語法

GScript 的語法與常見的 Java/Go 類似,所以上手非常簡單。

基本型別

先來看看基本型別,目前支援 int/string/float/bool 四種基本型別以及 nil 特殊型別。

變數宣告語法和 Java 類似:

int a=10;
string b,c;
float e = 10.1;
bool f = false;

個人覺得將型別放在前面,程式碼閱讀起來會更清晰一些,當然這也是個人喜好。

陣列

// 宣告並初始化
int[] a={1,2,3};
println(a);

// 宣告一個空陣列並指定大小
int[] table = [4]{};

println();
// 向陣列 append 資料
a = append(a,4);
println(a);
for(int i=0;i<len(a);i++){
	println(a[i]);
}

// 通過下標獲取陣列資料
int b=a[2];
println(b);

其實嚴格來講這並不算是陣列,因為它的底層是用 Go 切片實現的,所以可以動態擴容。

以這段程式碼為例:

int[] a=[2]{};
println("陣列大小:"+len(a));
a = append(a,1);
println("陣列大小:"+len(a));
println(a);
a[0]=100;
println(a);

輸出:

陣列大小:2
陣列大小:3
[<nil> <nil> 1]
[100 <nil> 1]

Class

類的支援非常重要,是實現物件導向的基礎,目前還未完全實現物件導向,只實現了資料與函數的封裝。

class ListNode{
    int value;
    ListNode next;
    ListNode(int v, ListNode n){
        value =v;
        next = n;
    }
}

// 呼叫建構函式時不需要使用 new 關鍵字。
ListNode l1 = ListNode(1, nil);

// 使用 . 呼叫物件屬性或函數。
println(l1.value);

預設情況下 class 具有無參建構函式:

class Person{
	int age=10;
	string name="abc";
	int getAge(){
		return 100+age;
	}
}

// 無參建構函式
Person xx= Person();
println(xx.age);
assertEqual(xx.age, 10);
println(xx.getAge());
assertEqual(xx.getAge(), 110);

得益於 class 的實現,結合剛才的陣列也可以定義出自定義型別的陣列:

// 大小為 16 的 Person 陣列
Person[] personList = [16]{};

函數

函數其實分為兩類:

  • 普通的全域性函數。
  • 類的函數。

本質上沒有任何區別,只是所屬範圍不同而已。

// 判斷連結串列是否有環
bool hasCycle(ListNode head){
    if (head == nil){
        return false;
    }
    if (head.next == nil){
        return false;
    }

    ListNode fast = head.next;
    ListNode slow = head;
    bool ret = false;
    for (fast.next != nil){
        if (fast.next == nil){
            return false;
        }
        if (fast.next.next == nil){
            return false;
        }
        if (slow.next == nil){
            return false;
        }
        if (fast == slow){
            ret = true;
            return true;
        }

        fast = fast.next.next;
        slow = slow.next;
    }
    return ret;
}

ListNode l1 = ListNode(1, nil);
bool b1 =hasCycle(l1);
println(b1);
assertEqual(b1, false);

ListNode l4 = ListNode(4, nil);
ListNode l3 = ListNode(3, l4);
ListNode l2 = ListNode(2, l3);
bool b2 = hasCycle(l2);
println(b2);
assertEqual(b2, false);

l4.next = l2;
bool b3 = hasCycle(l2);
println(b3);
assertEqual(b3, true);

這裡演示了連結串列是否有環的一個函數,只要有其他語言的使用基礎,相信閱讀起來沒有任何問題。

add(int a){}

當函數沒有返回值時,可以宣告為 void 或直接忽略返回型別。

閉包

閉包我認為是非常有意思的一個特性,可以實現很靈活的設計,也是函數語言程式設計的基礎。

所以在 GScript 中函數是作為一等公民存在;因此 GScript 也支援函數型別的變數。

函數變數宣告語法如下:func typeTypeOrVoid '(' typeList? ')'

// 外部變數,全域性共用。
int varExternal =10;
func int(int) f1(){
	// 閉包變數對每個閉包單獨可見
	int varInner = 20;
	int innerFun(int a){
		println(a);
		int c=100;
		varExternal++;
		varInner++;
		return varInner;
	}
	// 返回函數
	return innerFun;
}

// f2 作為一個函數型別,接收的是一個返回值和引數都是 int 的函數。
func int(int) f2 = f1();
for(int i=0;i<2;i++){
	println("varInner=" + f2(i) + ", varExternal=" + varExternal);
}
println("=======");
func int(int) f3 = f1();
for(int i=0;i<2;i++){
	println("varInner=" + f3(i) + ", varExternal=" + varExternal);
}

最終輸出如下:

0
varInner=21, varExternal=11
1
varInner=22, varExternal=12
=======
0
varInner=21, varExternal=13
1
varInner=22, varExternal=14
func int(int) f2 = f1();

以這段程式碼為例:f2 是一個返回值,入參都為 int 的函數型別;所以後續可以直接當做函數呼叫 f2(i).

例子中將閉包分別賦值給 f2 和 f3 變數,這兩個變數中的閉包資料也是互相隔離、互不影響的,所有基於這個特性甚至還是實現物件導向。

關於閉包的實現,後續會單獨更新一篇。

更多樣例請參考:https://github.com/crossoverJie/gscript/tree/main/example

標準庫

標準庫原始碼:https://github.com/crossoverJie/gscript/tree/main/internal

目前實現的標準庫並不多,這完全是一個體力活;基於現有的語法和基礎資料型別,幾乎可以實現大部分的資料結構了,所以感興趣的朋友也歡迎來貢獻標準庫程式碼;比如 StackSet 之類的資料結構。

MapString

以這個 MapString 為例:鍵值對都為 stringHashMap

int count =100;
MapString m1 = MapString();
for (int i=0;i<count;i++){
	string key = i+"";
	string value = key;
	m1.put(key,value);
}
println(m1.getSize());
assertEqual(m1.getSize(),count);

for (int i=0;i<count;i++){
	string key = i+"";
	string value = m1.get(key);
	println("key="+key+ ":"+ value);
	assertEqual(key,value);
}

使用起來和 JavaHashMap 類似,當然他的實現原始碼也是參考的 jdk1.7 的 HashMap

由於目前並有一個類似於 Java 的 object 或者是 go 中的 interface{}, 所以如果需要存放 int,那還得實現一個 MapInt,不過這個通用型別很快會實現。

內建函數

int[] a={1,2,3};
// len 返回陣列大小
println(len(a));

// 向陣列追加資料
a = append(a,4);
println(a);
// output: [1,2,3,4]

// 斷言函數,不相等時會丟擲執行時異常,並中斷程式。
assertEqual(len(a),4);

// 返回 hashcode
int hashcode = hash(key);

也內建了一些基本函數,當然也這不是由 GScript 原始碼實現的,而是編譯器實現的,所以新增起來要稍微麻煩一些;後續會逐步完善,比如和 IO 相關的內建函數。

總結

現階段的 GScript 還有許多功能沒有完善,比如 JSON、網路庫、更完善的語法檢查、編譯報錯資訊等;現在拿來刷刷 LeetCode 還是沒有問題的。

從這 65 個 todo 就能看出還有很長的路要走,我對它的終極目標就是可以編寫一個網站那就算是一個成熟的語言了。

目前還有一個問題是沒有整合式開發環境,現在的開發體驗和白板上寫程式碼相差無異,所以後續有時間的話嘗試寫一個 VS Code 的外掛,至少能有語法高亮與提示。

最後對 GScript 或者是編譯原理感興趣的小夥伴可以加我微信一起交流。

專案原始碼:https://github.com/crossoverJie/gscript

下載地址:https://github.com/crossoverJie/gscript/releases/tag/v0.0.6