手寫程式語言-實現運算子過載

crossoverJie發表於2022-09-19

先帶來日常的 GScript 更新:新增了可變引數的特性,語法如下:

int add(string s, int ...num){
    println(s);
    int sum = 0;
    for(int i=0;i<len(num);i++){
        int v = num[i];
        sum = sum+v;
    }
    return sum;
}
int x = add("abc", 1,2,3,4);
println(x);
assertEqual(x, 10);

得益於可變引數,所以新增了格式化字串的內建函式:

//formats according to a format specifier and writes to standard output.
printf(string format, any ...a){}

//formats according to a format specifier and returns the resulting string.
string sprintf(string format, any ...a){}

下面重點看看 GScript 所支援的運算子過載是如何實現的。

運算子過載其實也是多型的一種表現形式,我們可以重寫運算子的過載函式,從而改變他們的計算規則。

println(100+2*2);

以這段程式碼的運算子為例,輸出的結果自然是:104.

但如果我們是對兩個物件進行計算呢,舉個例子:

class Person{
    int age;
    Person(int a){
        age = a;
    }
}
Person p1 = Person(10);
Person p2 = Person(20);
Person p3 = p1+p2;

這樣的寫法在 Java/Go 中都會報編譯錯誤,這是因為他們兩者都不支援運算子過載;

Python/C# 是支援的,相比之下我覺得 C# 的實現方式更符合 GScript 語法,所以參考 C# 實現了以下的語法規則。

Person operator + (Person p1, Person p2){
    Person pp = Person(p1.age+p2.age);
    return pp;
}
Person p3 = p1+p2;
println("p3.age="+p3.age);
assertEqual(p3.age, 30);

有幾個硬性條件:

  • 函式名必須是 operator
  • 名稱後跟上運算子即可。

目前支援的運算子有:+-*/ == != < <= > >=

以前在使用 Python 運算子過載時就有想過它是如何實現的?但沒有深究,這次藉著自己實現相關功能從而需要深入理解。

其中重點就為兩步:

  1. 編譯期間:記錄所有的過載函式和運算子的關係。
  2. 執行期:根據當前的運算找到宣告的函式,直接執行即可。

第一步的重點是掃描所有的過載函式,將過載函式與運算子存放起來,需要關注的是函式的返回值與運算子型別。

// OpOverload 過載符
type OpOverload struct {
    function  *Func
    tokenType int
}

// 運算子過載自定義函式
opOverloads []*symbol.OpOverload

在編譯器中使用一個切片存放。

而在執行期中當兩個入參型別相同時,則需要查詢過載函式。

// GetOpFunction 獲取運算子過載函式
// 透過返回值以及運算子號(+-*/) 匹配過載函式
func (a *AnnotatedTree) GetOpFunction(returnType symbol.Type, tokenType int) *symbol.Func {
    for _, overload := range a.opOverloads {
        isType := overload.GetFunc().GetReturnType().IsType(returnType)
        if isType && overload.GetTokenType() == tokenType {
            return overload.GetFunc()
        }
    }
    return nil
}

查詢方式就是透過編譯期存放的資料進行匹配,拿到過載函式後自動呼叫便實現了過載。

感興趣的朋友可以檢視相關程式碼:

運算子過載其實並不是一個常用的功能;因為會改變運算子的語義,比如明明是加法卻在過載函式中寫為減法。

這會使得程式碼閱讀起來困難,但在某些情況下我們又非常希望語言本身能支援運算子過載。

比如在 Go 中常用的一個第三方精度庫decimal.Decimal,進行運算時只能使用 d1.Add(d2) 這樣的函式,當運算複雜時:

a5 = (a1.Add(a2).Add(a3)).Mul(a4);
a5 = (a1+a2+a3)*a4;

就不如下面這種直觀,所以有利有弊吧,多一個選項總不是壞事。

GScript 原始碼:
github.com/crossoverJie/gscript

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章