在 ToyLang 裡,字串是基本型態,使用成對的單引號 '' 含括(在 ToyLang 中不使用雙引號來表示字串,理由後述),例如 'Justin' 是個字串,如果想要在字串中表示單引號,必須使用 \' 表示,例如:
println('My name is Justin')     # 字串使用 '' 
println('My name is \'Justin\'') # 顯示 My name is 'Justin'
ToyLang 定義了幾個 escape 表示:
- \\反斜線
- \'單引號 '
- \n換行
- \r歸位
- \tTab
'' 含括的字串是基本型態,如果使用 typeof 函式的話,會傳回 'string':
println(typeof('Justin'))  # 顯示 string
println(typeof(123))       # 顯示 number
println(typeof(true))      # 顯示 boolean
以上也一併示範了 typeof 對數值與布林值分別會傳回 'number'、'boolean'。
可以使用 + 來串接字串:
firstName = 'Justin'
lastName = 'Lin'
println(firstName + ' ' + lastName)
如果想要知道字串的長度,可以將之包裹為 String 物件,這時就有 length 方法可以使用:
name = new String('Justin')
println(name.length())      # 顯示 6
當然這有些不方便,因此若字串接上 . 運算子試圖進行方法呼叫時,會自動包裹為 String 實例:
println('Justin'.length())  # 顯示 6
基本型態字串與 String 實例是不同的,這可以從 typeof 的結果得知:
println(typeof('Justin'))             # 顯示 string
println(typeof(new String('Justin'))) # 顯示 String
類別本身定義的方法,可以透過 ownMethods 取得,String 上可以使用的方法,可以透過以下程式碼來得知:
String.ownMethods().forEach(println)
這會顯示以下的內容:
<Function init>
<Function toUpperCase>
<Function toLowerCase>
<Function toString>
<Function trim>
<Function charAt>
<Function charCodeAt>
<Function codePointAt>
<Function endsWith>
<Function startsWith>
<Function includes>
<Function indexOf>
<Function lastIndexOf>
<Function substring>
<Function slice>
<Function split>
<Function length>
<Function format>
這部份主要對應至 JavaScript 的 String,因此方法簽署大致上是相同的,format 方法則是可用來進行簡單的字串格式化,例如:
println('Hello {0}'.format('World'))
println(String.format('Hello {0}', 'World'))
如同〈數值與布林〉,要識別 ToyLang 程式碼中,哪些符號的組合代表字串,才能抽取出該符號組合,進一步建立為語法樹的節點,這看來簡單,因為字串中允許各種符號,然而,必須考慮 \'、\n 等 escape 的問題,因而 Regular expression 可以是:
const TEXT_REGEX = /'((?:[^'\\]|\\'|\\\\|\\r|\\n|\\t)*)'/;
這個 Regular expression 只考慮單引號含括字串的情況,像 Python、JavaScript 等動態定型語言,也可以使用雙引號 "" 來表示字串,如果你也想要這麼做,可以將之試著加入至上頭的 Regular expression,然而,為了令 Regular expression 比較好讀一些,我暫時就沒這麼做了。
當比對到 ToyLang 寫的字串時,例如在 ToyLang 中寫 'Hello\tWorld',依上頭的 Regular expression 比對到的 JavaScript 字串值會是 'Hello\\tWorld',\t 這類控制字元有著實際作用,因而必須將 'Hello\\tWorld' 處理為 'Hello\tWorld',這可以在 expr_parser.js 中看到:
...
return Primitive.of(textTokenable.value
    .replace(/^\\r/, '\r')
    .replace(/([^\\])\\r/g, '$1\r')    
    .replace(/^\\n/, '\n')
    .replace(/([^\\])\\n/g, '$1\n')
    .replace(/^\\t/, '\t')
    .replace(/([^\\])\\t/g, '$1\t')
    .replace(/\\\\/g, '\\')
    .replace(/\\'/g, '\'')
...
話說,至今為止你看過幾次註解的運用,註解看似處理簡單,像是若為單獨的一行註解:
# 這是一個註解
println('Hello, World')  
剖析器處理時,只要看到 # 開頭,直接略過該行就好了,然而,若允許註解出現在程式碼之後呢?
println('Hello, World')  # 這是一個註解
也許你會認為,不就是 /#.*/ 比對出來,而後將之消去嗎?問題在於,如果註解前的程式碼中,有字串中包含 # 呢?
println('# 用來表示註解')  # 這是一個註解
要在程式碼後放單行註解的話,就不免遇到這類情況,你可以試著建立 Regular expression 看看,要考慮到的情況遠比你想像的複雜的多,我試著建立過幾個,然而總有考慮不周詳的情況,本來想寫程式來判斷了,不過後來找到〈Java Regex find Oracle Single Line comments Except in a String〉,根據當中的說明試著修改了一下,最後是使用 /^((?:(?!#|').|'(?:''|[^'])*')*)\s*#.*$/,說明可以在 tokenizer.js 中看到:
/* 
    Matching single line comments except in a string
    ref: https://stackoverflow.com/questions/8446064/java-regex-find-oracle-single-line-comments-except-in-a-string
    ^                    # match the start of the line
    (                    # start match group 1
    (?:                  #   start non-capturing group 1
        (?!#|').         #     if there's no '#' or single quote ahead, match any char
        |                #     OR
        '(?:''|[^'])*'   #     match a string literal
    )*                   #   end non-capturing group 1 and repeat it zero or more times
    )                    # end match group 1
    #.*$                 # match a comment all the way to the end of the line    
*/
在程式碼的處理上,使用了 replace 將抓到非註解部份,整個取代原有的行:
line.replace(/^((?:(?!#|').|'(?:''|[^'])*')*)\s*#.*$/, '$1')
字串基本型態,可以自動包裹為 String 實例的這個部份,主要必須知道 . 運算子是如何運作的,這部份之後文件才會說明,不過現階段可以知道的是,用來包裹基本型態的 Primitive 節點(位於 value.js 中),提供了一個 box 方法:
// number, text, boolean
class Primitive extends Value {
    constructor(value) {
        super();
        this.value = value;
    }
    toString() {
        return `${this.value}`;
    }
    // currently only support text
    box(context) {
        return newInstance(context, 'String', Primitive, this.value);
    }
    ...
}
在 . 運算子處理時,會呼叫這個 box 方法,目前 box 僅支援包裹為 String,因此,如果試著在數值、布林後接下 . 運算子呼叫方法是會出錯的(畢竟也沒定義對應的包裹類別,自然也不會有方法可以呼叫),有興趣的話,你可以試著定義對應的基本型態包裹類別。
有些函式或方法,底層直接呼叫了 JavaScript 環境的 API,它們被實現在 builtin 裏的相關模組之中,這部份設計為可以自由增減,日後文件談到物件導向的實現時,就會知道如何自行增加這些 API。
不過,老是將函式或方法,直接對應至底層 JavaScript 的 API 沒有意思,因而後來,除了真的必須得用到 JavaScript 環境的一些 API 之外,我就直接用 ToyLang 來寫函式或方法了,這些可以在 lib 之中看到。
你已經看過 ToyLang 中幾個 .js 的程式碼實現片段了,目的是讓你大致知道語法節點的長相,或者是一些處理語法時該注意的細節,通常也是書或文件中少提及的地方(像是註解怎麼處理)。
如果你沒有實現過語言的經驗,一下子要理解這麼多著實不易,這就是我留著 simple_lang.js 的原因。
建議試著從簡單的語言開始,另一個起點也許是〈Interpreter 模式〉,事實上,它也是 simple_lang.js 的起點。
《深入理解運算原理》第二章使用 Ruby 實現了一個簡單的語言,雖然是側重在抽象語法樹的說明,然而對於什麼是語法節點、何為運算式、陳述句又該如何串接清楚交代,可以參考一下。
只不過,運算式可不只有加減乘除這幾個二元運算子,ToyLang 就有近 20 個運算子,包含了單元、二元與三元運算子,我之後會談談自己是怎麼處理這些運算子的!

