import、import as、from import


ToyLang 的 lib 中定義了幾個模組,可以使用 import 匯入這些模組,例如,匯入 sys 模組:

import '/lib/sys'

sys.ownProperties().forEach(println)
println(sys.currentTimeMillis())

import 時必須加上 lib 的原因在於,〈Hello, World! Hello, Toy!〉中將 TOY_MODUEL_PATH 設為 'toy_lang',而 import 的路徑起點是從 TOY_MODUEL_PATH 起算,就目前的設定來說,import '/lib/sys' 會使用 lib 中的 sys.toy 定義之模組。

每個模組在 ToyLang 中,都是 Module 的實例,模組的主檔名會作為模組名稱,使用 import 時,模組的主檔名也會成為目前環境中的變數名稱。

取得 Module 的實例之後,因此透過它來取用模組中公開的變數、函式、類別等,上面的範例會顯示:

[currentTimeMillis,<Function currentTimeMillis>]
[loadedModules,<Function loadedModules>]
[unhandledExceptionHandler,<Function unhandledExceptionHandler>]
1536131150673

如果想要改變被匯入模組在當前環境中的變數名稱,可以使用 import as。例如:

import '/lib/sys' as system

println(system.currentTimeMillis())
println(system)

import as 改變的是被匯入模組在目前環境中的名稱,而不是模組實例之名稱,例如以上會顯示:

import '/lib/sys' as system

println(system.currentTimeMillis())
println(system)

如果不想透過名稱來存取模組中公開的函式等元素,可以使用 from import 語句。例如:

from '/lib/sys' import currentTimeMillis

println(currentTimeMillis())

ToyLang 的模組有點像是 JavaScript 的情況,JavaScript 在 ES6 前並沒有模組管理功能,因此有著各種模擬模組的方式,也有各種模組管理程式庫。

也就是說,ToyLang 的模組功能是最後才加上去的,而且特意仿造 JavaScript 在 ES6 前的情況,嚴格來說,並不是語言本身的一部份,它並沒有參與語法樹,雖然為了使用上比較方便,這是在語法上提供了 importimport asfrom import,也定義了模組對應的 Module 類別,然而除此之外,實作上比較像是個獨立的模組管理程式庫。

因為實作時的執行環境,都是在瀏覽器中,因此在取得模組檔案的部份,是透過 Fetch API,這部份主要都是集中在 js/module.js 之中。

簡單來說,ToyLang 本來就是以一個 .toy 檔案為單位在解析執行,只要取得 .toy 檔案,同樣地經由剖析、建立語法樹、執行,就是模組的處理方式,這些定義在 Module

class Module {
    constructor(fileName, moduleName, notImports, importers = []) {
        this.fileName = fileName;
        this.moduleName = moduleName;
        this.notImports = notImports;
        this.importers = importers;
    }

    ... 略
}

notImports 指的是開頭 importimport asfrom import 等以外的語句,因此,ToyLang 中,importimport asfrom import 只能寫在檔案開頭。

Module 主要負責使用 Fetch API 載入 .toy 檔案,剖析、建立語法樹、執行,最後得到 ToyLang 中 Module 類別的實例,這些動作都是從 run 方法作為起點:

...
static run(fileName, code) {
    const lines = tokenizer(code).tokenizableLines();
    const notImports = notImportTokenizableLines(lines);
    const imports = importTokenizableLines(lines);

    if(imports.length !== 0) {
        Promise.all(importPromises(fileName, imports))
               .then(importers => new Module(fileName, moduleNameFrom(fileName), notImports, importers).play());
    }
    else {
        new Module(fileName, moduleNameFrom(fileName), notImports).play();
    }        
}

我曾經為了瞭解 RequireJS 而寫了個 RequireJS-Toy 原型,在 ToyLang 使用 Fetch API 載入 .toy 檔案的這部份,寫過該原型的幫助很大,在瀏覽器的環境,想要以非同步載入某些資源,又必須兼顧資源間順序的情況,RequireJS-Toy 是個可以參考的簡單專案。

除去 Module 使用 Fetch API 載入 .toy 檔案的部份,剖析、建立語法樹、執行的部份,基本上與沒有加入模組功能之前的作法,幾乎是沒有兩樣的。

因為關於 importimport asfrom import,完全是獨立在 ModuleImporter 之前進行:

class ModuleImporter {
    constructor(sourceModule, type = 'default', name) {
        this.sourceModule = sourceModule;
        this.type = type;
        this.name = name;
    }

    importTo(context) {
        const moduleInstance = this.sourceModule.moduleInstance();
        switch(this.type) {
            case 'variableName':   // from '...' import foo
                context.variables.set(this.name, moduleInstance.properties.get(this.name));
                break;
            case 'all':            // from '...' import *
                Array.from(moduleInstance.properties.entries())
                     .forEach(entry => context.variables.set(entry[0], entry[1]));
                break;
            case 'moduleName':     // import '...' as name
                context.variables.set(this.name, moduleInstance);
                break;
            default:               // import '....'
                context.variables.set(this.sourceModule.moduleName, moduleInstance);
                break;
        }
    }
}

說穿了也不難,就只是在目前環境物件中,必須設定哪些變數罷了;另外,每個模組在 ToyLang 中,都是 Module 類別的實例,這是定義在 builtin\classes\module.js 之中:

class ModuleClass {}

ModuleClass.methods = new Map([
    ['name', func0('name', {
        evaluate(context) {
            const ctxNode = selfInternalNode(context);
            return context.returned(new Primitive(ctxNode.moduleName));
        }    
    })],
    ['toString', func0('toString', {
        evaluate(context) {
            const clzNode = self(context).clzNodeOfLang();
            const ctxNode = selfInternalNode(context);
            return context.returned(new Primitive(`<${clzNode.name} ${ctxNode.moduleName}>`));
        }    
    })]
]);

主要就是提供模組名稱的 name 方法,並有 toString 方法實作,其他方法都是從 Object 繼承而來。