數值與布林


在 ToyLang 中,區分基本型態與複合型態,基本型態有數值、布林與字串,複合型態就是常稱的物件,在這邊先談談數值與布林。

ToyLang 中的數值沒有整數與浮點數的區別,數值都是 IEEE 754 標準 64 位元浮點數,可表示的數字最小值為 Number.MIN_VALUE,而最大值為 Number.MAX_VALUE,可使用的安全整數範圍為 Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER

撰寫數值時要使用數值實字(Number literal),預設是十進位整數,也可以用 0x 開頭表示 16 進位整數、0o 表示 8 進位整數,使用 0b 表示 2 進位整數。例如:

  • 10 (10 進位)
  • 0xFF (16 進位)
  • 0o77 ( 8 進位)
  • 0b11 ( 2 進位)

若要表示浮點數,也可使用科學記號表示。例如:

  • 3.14
  • 5.231e13
  • 1.31e-32

顯然地,Number 是個名稱空間,除了可以用來取得一些常數之外,還有 Number.parseIntNumber.parseFloat 可以使用。

println(Number.parseInt('10'))         # 顯示 10 
println(Number.parseInt('10', 8))      # 顯示 8
println(Number.parseInt('10', 2))      # 顯示 2
println(Number.parseFloat('3.14e10'))  # 顯示 31400000000

你看到的 # 符號是用來撰寫註解之用,可以放在單獨的一行,或者是單行程式碼的後頭。

布林值只有兩個值,truefalse,分別表示真與假。


為什麼 ToyLang 的數值是浮點數呢?我偷懶了,直接將數值對應至 JavaScript 的數值,而 JavaScript 的數值就是使用浮點數來表示。

如果要精實一些的話,是可以區分整數與浮點數,在開發一門語言時,對於每個「值」,必須有個語法節點,以 simple_lang.js 中的 Num 為例:

class Num {
    constructor(value) {
        this.value = value;
    }

    evaluate(context) {
        return this;
    }
}

基本型態的節點很單純,封裝了某個底層值,例如 new Num(1) 是用 Num 實例封裝了 JavaScript 的數值 1,new Num(3.14) 封裝了 JavaScript 的數值 3.14。

evaluate 是運行語法節點時會用到的方法,對基本型態來說,單純傳回物件本身,方法的呼叫者,就可以透過傳回值的 value 特性取得底層數值,之後的文件會談到運算式的組合,屆時就會看到 evaluate 的運用。

如果想要區分整數與浮點數的話,可以分別定義 IntFloat 節點:

class Int {
    constructor(value) {
        this.value = value;
    }

    evaluate(context) {
        return this;
    }
}

class Float {
    constructor(value) {
        this.value = value;
    }

    evaluate(context) {
        return this;
    }
}

這時 Int 只接受 1、2、3 這類的底層整數,Float 可接受 3.14 這類的浮點數,如果今天是使用一個強型別語言來實作 ToyLang,例如 Python,那麼區分整數與浮點數的動作,就會是必要的,因為在數值的自動型態轉換上受到嚴格的限制,IntFloat 在處理上必然不同。

然而,因為目前 ToyLang 使用 JavaScript 來實現,而 JavaScrip 的自動型態轉換是出了名的方便(或者說難搞),如上區分 IntFloat 意義上較小,因此我就合併,在 simple_lang.js 中是使用一個 Num 來代表數值。

後面的文件會看到,在 ToyLang 中,更進一步地,我將基本型態都使用 Primitive 來表示。

注意,這邊談到的 NumIntFloat 等,並不是 ToyLang 中的類別,而只是實現語法節點時,原生語言用來表示語法節點的類別,要談到 ToyLang 中的類別,還有很長的一段路要走。

布林值也是類似的,只不過因為布林值只有 truefalse 兩個值,因此在語法節點上,可以直接產生兩個物件,以 simple_lang.js 的做法來說..

const BOOL_TRUE = new Num(true)
const FALSE_TRUE = new Num(false)

實現語言必然要有語法剖析器的存在,簡單來說,任何程式碼對語言實作品來說,不過就是一個或長或短的字串,語法剖析器的目的,就是辨識字串,並將之轉換為正確的語法節點。

撰寫剖析器很麻煩,有些介紹實作語言的文件或書,都會使用現成的剖析器產生器,以便簡化,不過,這並不表示你不用知道剖析器的原理,或者不用思考文法等細節。

對我來說,認識如何語法剖析器基本上如何實現,也是課題之一,因此我自行實現了簡單的語法剖析器,大量地使用 Regular expression 進行字串的辨識,技巧上應該是很不成熟或欠缺效率,然而可以運作,也從中知道了許多剖析器設計上該注意的事。

就目前這文件的主題來說,要知道如何剖析出數值與布林值,布林值很簡單,只要比對 'true''false' 字串,因而 Regular expression 只要:

const BOOLEAN_REGEX = /true|false/;

對於數值來說就複雜一些了,最基本的是必須考慮 0 到 9 與小數點,也就是 /[0-9]+\.?[0-9]*/,不過像 0x0o0b 也要考慮的話,就會是 /0[box][0-9A-F]+|[0-9]+\.?[0-9]*/,最後考量科學記號,因此最後的 Regular expression 是:

const NUMBER_REGEX = /0[box][0-9A-F]+|[0-9]+\.?[0-9e+-]*/;

由於 ToyLang 大量使用 Regular expression 來進行比對,為了便於管理 Regular expression,絕大多數的 Regular expression 被集中在 regex.js

Regular expression 基本上不易閱讀也不易維護,因而可以的情況下,我會重用已經定義的 Regular expression,將之重組為更複雜的 Regular expression,這對剖析上沒有效率,然而對理解上有很大的幫助。