項(xiàng)目:一門(mén)編程語(yǔ)言
求值器是確定編程語(yǔ)言中表達(dá)式含義的程序,它本身也只是另一個(gè)程序。
——哈爾·阿貝爾森與杰拉爾德·薩斯曼,《計(jì)算機(jī)程序的構(gòu)造和解釋》
(插圖:一個(gè)帶孔的雞蛋,內(nèi)部可見(jiàn)更小的雞蛋,而這些小雞蛋內(nèi)部又有甚至更小的雞蛋,以此類(lèi)推)
構(gòu)建屬于自己的編程語(yǔ)言出奇地簡(jiǎn)單(只要目標(biāo)不設(shè)得太高),而且極具啟發(fā)性。
本章我想展示的核心觀點(diǎn)是:構(gòu)建編程語(yǔ)言并不存在魔法。我常常覺(jué)得某些人類(lèi)發(fā)明極其巧妙復(fù)雜,以至于自認(rèn)為永遠(yuǎn)無(wú)法理解。但經(jīng)過(guò)少量閱讀和實(shí)踐后會(huì)發(fā)現(xiàn),它們往往非常平常。
我們將構(gòu)建一門(mén)名為Egg的編程語(yǔ)言。它會(huì)是一門(mén)極小且簡(jiǎn)單的語(yǔ)言,但卻強(qiáng)大到足以表達(dá)你能想到的任何計(jì)算。它將支持基于函數(shù)的簡(jiǎn)單抽象機(jī)制。
解析
編程語(yǔ)言最直觀可見(jiàn)的部分是其語(yǔ)法(即表示法)。解析器是一種程序,它讀取一段文本并生成反映該文本中程序結(jié)構(gòu)的數(shù)據(jù)結(jié)構(gòu)。如果文本無(wú)法構(gòu)成有效程序,解析器應(yīng)指出錯(cuò)誤。
我們的語(yǔ)言將采用簡(jiǎn)單且統(tǒng)一的語(yǔ)法。在Egg中一切都是表達(dá)式。表達(dá)式可以是綁定名稱、數(shù)字、字符串或應(yīng)用。應(yīng)用既用于函數(shù)調(diào)用,也用于if或while等結(jié)構(gòu)。
為保持解析器的簡(jiǎn)潔,Egg中的字符串不支持任何類(lèi)似反斜杠轉(zhuǎn)義的功能。字符串僅是用雙引號(hào)包裹的非雙引號(hào)字符序列。數(shù)字是數(shù)字序列。綁定名稱可由任何非空白且在語(yǔ)法中無(wú)特殊含義的字符組成。
應(yīng)用的書(shū)寫(xiě)方式與JavaScript類(lèi)似,即在表達(dá)式后添加括號(hào),并在括號(hào)內(nèi)放入任意數(shù)量的參數(shù)(以逗號(hào)分隔)。
do(define(x, 10),
if(>(x, 5),
print("large"),
print("small")))
Egg語(yǔ)言的統(tǒng)一性意味著,在JavaScript中作為運(yùn)算符的符號(hào)(如>)在該語(yǔ)言中是普通綁定,其應(yīng)用方式與其他函數(shù)相同。由于語(yǔ)法中沒(méi)有代碼塊的概念,我們需要用do結(jié)構(gòu)來(lái)表示按順序執(zhí)行多個(gè)操作。
解析器用于描述程序的數(shù)據(jù)結(jié)構(gòu)由表達(dá)式對(duì)象組成,每個(gè)對(duì)象都有一個(gè)type屬性(指示表達(dá)式類(lèi)型)和其他屬性(描述其內(nèi)容)。
類(lèi)型為"value"的表達(dá)式表示字面值字符串或數(shù)字,其value屬性包含所表示的字符串或數(shù)字值。類(lèi)型為"word"的表達(dá)式用于標(biāo)識(shí)符(名稱),此類(lèi)對(duì)象的name屬性以字符串形式存儲(chǔ)標(biāo)識(shí)符名稱。最后,"apply"類(lèi)型表達(dá)式表示應(yīng)用,其operator屬性指向被應(yīng)用的表達(dá)式,args屬性則存儲(chǔ)參數(shù)表達(dá)式數(shù)組。
前面程序中的>(x, 5)部分可表示為:
{
type: "apply",
operator: {type: "word", name: ">"},
args: [
{type: "word", name: "x"},
{type: "value", value: 5}
]
}
這種數(shù)據(jù)結(jié)構(gòu)被稱為語(yǔ)法樹(shù)。如果將對(duì)象視為節(jié)點(diǎn)、對(duì)象間的關(guān)聯(lián)視為節(jié)點(diǎn)間的連線(如下圖所示),該結(jié)構(gòu)會(huì)呈現(xiàn)樹(shù)狀形態(tài)。表達(dá)式中包含其他表達(dá)式(而后者可能再包含更多表達(dá)式)的特性,類(lèi)似于樹(shù)枝不斷分叉的形態(tài)。
(https://eloquentjavascript.net/img/syntax_tree.svg示意圖:示例程序的語(yǔ)法樹(shù)結(jié)構(gòu)。根節(jié)點(diǎn)標(biāo)記為'do',有兩個(gè)子節(jié)點(diǎn)分別標(biāo)記為'define'和'if',這些子節(jié)點(diǎn)又進(jìn)一步包含描述其內(nèi)容的子節(jié)點(diǎn))
這與我們?cè)诘?章為配置文件格式編寫(xiě)的解析器形成對(duì)比——后者結(jié)構(gòu)簡(jiǎn)單:將輸入拆分為行并逐行處理,且每行僅允許幾種簡(jiǎn)單形式。
此處我們需要采用不同的方法:表達(dá)式不按行分隔,且具有遞歸結(jié)構(gòu)——應(yīng)用表達(dá)式中包含其他表達(dá)式。
幸運(yùn)的是,這個(gè)問(wèn)題可以通過(guò)編寫(xiě)一個(gè)遞歸解析函數(shù)來(lái)完美解決,該函數(shù)的遞歸方式反映了語(yǔ)言的遞歸本質(zhì)。
我們定義一個(gè)parseExpression函數(shù),它接受字符串作為輸入,返回一個(gè)對(duì)象,其中包含字符串起始位置表達(dá)式的數(shù)據(jù)結(jié)構(gòu),以及解析該表達(dá)式后剩余的字符串部分。在解析子表達(dá)式(例如應(yīng)用的參數(shù))時(shí),可以再次調(diào)用此函數(shù),得到參數(shù)表達(dá)式及剩余文本。剩余文本可能包含更多參數(shù),或可能是結(jié)束參數(shù)列表的右括號(hào)。
以下是解析器的第一部分:
function parseExpression(program) {
program = skipSpace(program);
let match, expr;
if (match = /^"([^"]*)"/.exec(program)) {
expr = {type: "value", value: match[1]};
} else if (match = /^\d+\b/.exec(program)) {
expr = {type: "value", value: Number(match[0])};
} else if (match = /^[^\s(),#"]+/.exec(program)) {
expr = {type: "word", name: match[0]};
} else {
throw new SyntaxError("Unexpected syntax: " + program);
}
return parseApply(expr, program.slice(match[0].length));
}
function skipSpace(string) {
let first = string.search(/\S/);
if (first == -1) return "";
return string.slice(first);
}
由于Egg和JavaScript一樣允許元素之間存在任意數(shù)量的空白,因此我們需要不斷從程序字符串的開(kāi)頭剔除空白。skipSpace函數(shù)用于實(shí)現(xiàn)這一功能。
在跳過(guò)所有前導(dǎo)空白后,parseExpression使用三個(gè)正則表達(dá)式來(lái)識(shí)別Egg支持的三種原子元素:字符串、數(shù)字和標(biāo)識(shí)符。解析器會(huì)根據(jù)匹配的表達(dá)式類(lèi)型構(gòu)建不同的數(shù)據(jù)結(jié)構(gòu)。如果輸入不匹配這三種形式中的任何一種,則視為無(wú)效表達(dá)式,解析器將拋出錯(cuò)誤。這里我們使用SyntaxError構(gòu)造函數(shù)——這是標(biāo)準(zhǔn)定義的異常類(lèi)(類(lèi)似Error,但更具體)。
然后,我們從程序字符串中截取匹配的部分,并將其與表達(dá)式對(duì)象一起傳遞給parseApply,該函數(shù)會(huì)檢查表達(dá)式是否為應(yīng)用表達(dá)式。如果是,它會(huì)解析括號(hào)內(nèi)的參數(shù)列表。
function parseApply(expr, program) {
program = skipSpace(program);
if (program[0] != "(") {
return {expr: expr, rest: program};
}
program = skipSpace(program.slice(1));
expr = {type: "apply", operator: expr, args: []};
while (program[0] != ")") {
let arg = parseExpression(program);
expr.args.push(arg.expr);
program = skipSpace(arg.rest);
if (program[0] == ",") {
program = skipSpace(program.slice(1));
} else if (program[0] != ")") {
throw new SyntaxError("Expected ',' or ')'");
}
}
return parseApply(expr, program.slice(1));
}
如果程序中的下一個(gè)字符不是左括號(hào),則說(shuō)明這不是應(yīng)用表達(dá)式,parseApply將返回傳入的表達(dá)式。否則,它會(huì)跳過(guò)左括號(hào)并為該應(yīng)用表達(dá)式創(chuàng)建語(yǔ)法樹(shù)對(duì)象。然后遞歸調(diào)用parseExpression解析每個(gè)參數(shù),直到找到右括號(hào)。這種遞歸是間接的,通過(guò)parseApply和parseExpression相互調(diào)用實(shí)現(xiàn)。
由于應(yīng)用表達(dá)式本身可以被應(yīng)用(例如multiplier(2)(1)),因此parseApply在解析完一個(gè)應(yīng)用后,必須再次調(diào)用自身以檢查是否跟隨另一對(duì)括號(hào)。
這就是解析Egg所需的全部?jī)?nèi)容。我們將其封裝在一個(gè)便捷的parse函數(shù)中,該函數(shù)會(huì)驗(yàn)證解析完表達(dá)式后是否已到達(dá)輸入字符串的末尾(Egg程序是單個(gè)表達(dá)式),并為我們提供程序的數(shù)據(jù)結(jié)構(gòu)。
function parse(program) {
let {expr, rest} = parseExpression(program);
if (skipSpace(rest).length > 0) {
throw new SyntaxError("Unexpected text after program");
}
return expr;
}
console.log(parse("+(a, 10)"));
// → {type: "apply",
// operator: {type: "word", name: "+"},
// args: [{type: "word", name: "a"},
// {type: "value", value: 10}]}
它工作了!盡管在解析失敗時(shí)不會(huì)提供非常有用的信息,也不會(huì)存儲(chǔ)每個(gè)表達(dá)式起始的行和列(這在后續(xù)報(bào)告錯(cuò)誤時(shí)可能有用),但對(duì)于我們的需求來(lái)說(shuō)已經(jīng)足夠了。
求值器
我們能用程序的語(yǔ)法樹(shù)做什么?當(dāng)然是運(yùn)行它!這正是求值器的工作。將語(yǔ)法樹(shù)和一個(gè)把名稱映射到值的作用域?qū)ο髠鹘o它,它會(huì)求值該樹(shù)表示的表達(dá)式并返回計(jì)算結(jié)果。
const specialForms = Object.create(null);
function evaluate(expr, scope) {
if (expr.type == "value") {
return expr.value;
} else if (expr.type == "word") {
if (expr.name in scope) {
return scope[expr.name];
} else {
throw new ReferenceError(
`未定義的綁定: ${expr.name}`);
}
} else if (expr.type == "apply") {
let {operator, args} = expr;
if (operator.type == "word" &&
operator.name in specialForms) {
return specialForms[operator.name](expr.args, scope);
} else {
let op = evaluate(operator, scope);
if (typeof op == "function") {
return op(...args.map(arg => evaluate(arg, scope)));
} else {
throw new TypeError("應(yīng)用非函數(shù)值");
}
}
}
}
求值器為每種表達(dá)式類(lèi)型都提供了處理邏輯:
-
字面值表達(dá)式直接返回其值(例如表達(dá)式
100求值為數(shù)字100) - 綁定名稱需檢查是否在作用域中定義,存在則獲取對(duì)應(yīng)值
- 應(yīng)用表達(dá)式處理邏輯更復(fù)雜:若是特殊形式(如if),則直接將參數(shù)表達(dá)式和作用域傳給處理函數(shù);若是普通調(diào)用,先求值運(yùn)算符得到函數(shù),再用求值后的參數(shù)調(diào)用該函數(shù)
我們用純JavaScript函數(shù)值表示Egg的函數(shù)值,這一點(diǎn)會(huì)在定義fun特殊形式時(shí)詳細(xì)說(shuō)明。
evaluate的遞歸結(jié)構(gòu)與解析器結(jié)構(gòu)相呼應(yīng),兩者都鏡像了語(yǔ)言本身的結(jié)構(gòu)。也可以將解析器和求值器合并為一個(gè)函數(shù),在解析時(shí)直接求值,但拆分實(shí)現(xiàn)會(huì)讓程序更清晰靈活。
這就是解釋Egg所需的全部核心邏輯,就是這么簡(jiǎn)單!但如果不定義一些特殊形式并向環(huán)境添加有用值,這門(mén)語(yǔ)言目前還做不了太多事。
特殊形式
specialForms對(duì)象用于定義Egg的特殊語(yǔ)法,將名稱與求值這些形式的函數(shù)關(guān)聯(lián),目前它還是空的。先添加if形式:
specialForms.if = (args, scope) => {
if (args.length != 3) {
throw new SyntaxError("if參數(shù)數(shù)量錯(cuò)誤");
} else if (evaluate(args[0], scope) !== false) {
return evaluate(args[1], scope);
} else {
return evaluate(args[2], scope);
}
};
Egg的if結(jié)構(gòu)嚴(yán)格要求3個(gè)參數(shù):先求值第一個(gè)參數(shù),若結(jié)果不是false則求值第二個(gè)參數(shù),否則求值第三個(gè)參數(shù)。這種if形式更像JavaScript的三元運(yùn)算符?:——它是表達(dá)式而非語(yǔ)句,會(huì)產(chǎn)生一個(gè)值(第二個(gè)或第三個(gè)參數(shù)的求值結(jié)果)。
Egg與JavaScript的另一個(gè)區(qū)別是條件值處理:只有false會(huì)被視為假值,0或空字符串等不會(huì)被當(dāng)作假值。
必須將if表示為特殊形式而非普通函數(shù)的原因在于:函數(shù)的所有參數(shù)都會(huì)在調(diào)用前求值,而if應(yīng)根據(jù)第一個(gè)參數(shù)的值僅求值第二個(gè)或第三個(gè)參數(shù)。
while形式類(lèi)似:
specialForms.while = (args, scope) => {
if (args.length != 2) {
throw new SyntaxError("while參數(shù)數(shù)量錯(cuò)誤");
}
while (evaluate(args[0], scope) !== false) {
evaluate(args[1], scope);
}
// 因Egg中不存在undefined,返回false作為無(wú)意義結(jié)果
return false;
};
另一個(gè)基礎(chǔ)結(jié)構(gòu)是do,它按順序執(zhí)行所有參數(shù),返回最后一個(gè)參數(shù)的值:
specialForms.do = (args, scope) => {
let value = false;
for (let arg of args) {
value = evaluate(arg, scope);
}
return value;
};
為創(chuàng)建綁定并賦值,還需要define形式:它要求第一個(gè)參數(shù)是名稱,第二個(gè)參數(shù)是產(chǎn)生賦值的表達(dá)式。作為表達(dá)式,define需返回賦值的值(類(lèi)似JavaScript的=運(yùn)算符):
specialForms.define = (args, scope) => {
if (args.length != 2 || args[0].type != "word") {
throw new SyntaxError("define使用錯(cuò)誤");
}
let value = evaluate(args[1], scope);
scope[args[0].name] = value;
return value;
};
環(huán)境
evaluate接受的作用域是一個(gè)對(duì)象,其屬性名對(duì)應(yīng)綁定名稱,屬性值對(duì)應(yīng)綁定值?,F(xiàn)在定義表示全局作用域的對(duì)象:
為使用剛定義的if結(jié)構(gòu),需要訪問(wèn)布爾值。由于只有兩個(gè)布爾值,無(wú)需特殊語(yǔ)法,直接將兩個(gè)名稱綁定到true和false即可:
const topScope = Object.create(null);
topScope.true = true;
topScope.false = false;
現(xiàn)在可以求值一個(gè)簡(jiǎn)單的布爾取反表達(dá)式:
let prog = parse(`if(true, false, true)`);
console.log(evaluate(prog, topScope));
// → false
為提供基礎(chǔ)算術(shù)和比較運(yùn)算符,還需向作用域添加一些函數(shù)值。為簡(jiǎn)化代碼,用Function在循環(huán)中合成一組運(yùn)算符函數(shù)而非逐個(gè)定義:
for (let op of ["+", "-", "*", "/", "==", "<", ">"]) {
topScope[op] = Function("a, b", `return a ${op} b;`);
}
添加輸出功能也很有用,將console.log包裝為print函數(shù):
topScope.print = value => {
console.log(value);
return value;
};
現(xiàn)在具備了編寫(xiě)簡(jiǎn)單程序的基本工具。以下函數(shù)提供了便捷方式:解析程序并在新作用域中運(yùn)行:
function run(program) {
return evaluate(parse(program), Object.create(topScope));
}
我們將用對(duì)象原型鏈表示嵌套作用域,使程序能在局部作用域添加綁定而不修改頂層作用域。例如計(jì)算1到10之和的程序:
run(`
do(define(total, 0),
define(count, 1),
while(<(count, 11),
do(define(total, +(total, count)),
define(count, +(count, 1)))),
print(total))
`);
// → 55
雖然比等價(jià)的JavaScript程序更繁瑣,但對(duì)于用不到150行代碼實(shí)現(xiàn)的語(yǔ)言來(lái)說(shuō)已經(jīng)不錯(cuò)了。
函數(shù)
沒(méi)有函數(shù)的編程語(yǔ)言是殘缺的。好在添加fun結(jié)構(gòu)并不難:它將最后一個(gè)參數(shù)作為函數(shù)體,前面的參數(shù)作為函數(shù)參數(shù)名:
specialForms.fun = (args, scope) => {
if (!args.length) {
throw new SyntaxError("函數(shù)需要函數(shù)體");
}
let body = args[args.length - 1];
let params = args.slice(0, args.length - 1).map(expr => {
if (expr.type != "word") {
throw new SyntaxError("參數(shù)名必須是標(biāo)識(shí)符");
}
return expr.name;
});
return function(...args) {
if (args.length != params.length) {
throw new TypeError("參數(shù)數(shù)量錯(cuò)誤");
}
let localScope = Object.create(scope);
for (let i = 0; i < args.length; i++) {
localScope[params[i]] = args[i];
}
return evaluate(body, localScope);
};
};
Egg的函數(shù)有自己的局部作用域:fun生成的函數(shù)創(chuàng)建局部作用域并添加參數(shù)綁定,然后在該作用域中求值函數(shù)體并返回結(jié)果。
示例1:定義加1函數(shù)并調(diào)用:
run(`
do(define(plusOne, fun(a, +(a, 1))),
print(plusOne(10)))
`);
// → 11
示例2:遞歸定義冪函數(shù):
run(`
do(define(pow, fun(base, exp,
if(==(exp, 0),
1,
*(base, pow(base, -(exp, 1)))))),
print(pow(2, 10)))
`);
// → 1024
編譯
目前構(gòu)建的是解釋器,求值時(shí)直接操作解析器生成的程序表示。
編譯則是在解析和運(yùn)行之間增加一個(gè)步驟,通過(guò)提前完成盡可能多的工作,將程序轉(zhuǎn)換為更高效的執(zhí)行形式。例如在設(shè)計(jì)良好的語(yǔ)言中,無(wú)需運(yùn)行程序就能確定每個(gè)綁定引用的具體綁定,從而避免每次訪問(wèn)時(shí)按名稱查找,直接從預(yù)定內(nèi)存位置獲取。
傳統(tǒng)上,編譯指將程序轉(zhuǎn)換為機(jī)器碼(處理器可執(zhí)行的原始格式),但任何將程序轉(zhuǎn)換為不同表示的過(guò)程都可視為編譯。
可以為Egg實(shí)現(xiàn)另一種求值策略:先將程序轉(zhuǎn)換為JavaScript程序,用Function調(diào)用JavaScript編譯器,再運(yùn)行結(jié)果。正確實(shí)現(xiàn)的話,這會(huì)讓Egg運(yùn)行極快且實(shí)現(xiàn)簡(jiǎn)單。
若對(duì)該主題感興趣并愿花時(shí)間研究,建議嘗試實(shí)現(xiàn)這樣的編譯器作為練習(xí)。
取巧實(shí)現(xiàn)
定義if和while時(shí)可能注意到,它們本質(zhì)上是JavaScript對(duì)應(yīng)結(jié)構(gòu)的簡(jiǎn)單包裝,Egg的值也直接使用JavaScript值。要對(duì)接更底層的系統(tǒng)(如處理器理解的機(jī)器碼)需要更多工作,但原理與當(dāng)前實(shí)現(xiàn)類(lèi)似。
盡管本章的玩具語(yǔ)言在功能上不如JavaScript,但在某些場(chǎng)景下,編寫(xiě)小型語(yǔ)言能更高效地完成實(shí)際工作。
這種語(yǔ)言不一定要像傳統(tǒng)編程語(yǔ)言。例如若JavaScript沒(méi)有正則表達(dá)式,可自行編寫(xiě)正則表達(dá)式的解析器和求值器。
再比如構(gòu)建一個(gè)程序,允許通過(guò)提供語(yǔ)言的邏輯描述來(lái)快速創(chuàng)建解析器,就可以為該場(chǎng)景定義特定表示法和編譯器。例如:
expr = 數(shù)字 | 字符串 | 名稱 | 應(yīng)用
數(shù)字 = 數(shù)字+
名稱 = 字母+
字符串 = '"' (! '"')* '"'
應(yīng)用 = expr '(' (expr (',' expr)*)? ')'
這就是所謂的領(lǐng)域特定語(yǔ)言(DSL),專為表達(dá)特定領(lǐng)域知識(shí)設(shè)計(jì)。由于只描述該領(lǐng)域需要的內(nèi)容,DSL比通用語(yǔ)言更具表達(dá)力。
練習(xí)
數(shù)組
通過(guò)向topScope添加以下三個(gè)函數(shù)為Egg增加數(shù)組支持:
-
array(...values):構(gòu)造包含參數(shù)值的數(shù)組 -
length(array):獲取數(shù)組長(zhǎng)度 -
element(array, n):獲取數(shù)組第n個(gè)元素
topScope.array = Function("...values", "return values;");
topScope.length = Function("arr", "return arr.length;");
topScope.element = Function("arr, n", "return arr[n];");
測(cè)試程序:
run(`
do(define(sum, fun(array,
do(define(i, 0),
define(sum, 0),
while(<(i, length(array)),
do(define(sum, +(sum, element(array, i))),
define(i, +(i, 1)))),
sum))),
print(sum(array(1, 2, 3))))
`);
// → 6
閉包
Egg的fun定義方式允許函數(shù)引用外圍作用域,使函數(shù)體能使用定義時(shí)可見(jiàn)的局部值,這與JavaScript函數(shù)的閉包行為一致。
以下程序中,函數(shù)f返回的函數(shù)將自身參數(shù)與f的參數(shù)相加,這需要訪問(wèn)f作用域內(nèi)的綁定a:
run(`
do(define(f, fun(a, fun(b, +(a, b)))),
print(f(4)(5)))
`);
// → 9
原理在于:fun生成的函數(shù)通過(guò)localScope = Object.create(scope)繼承了定義時(shí)的作用域(scope參數(shù)是定義函數(shù)時(shí)的作用域)。當(dāng)內(nèi)部函數(shù)執(zhí)行時(shí),scope指向f調(diào)用時(shí)創(chuàng)建的局部作用域,因此能訪問(wèn)a的值。
注釋
為Egg添加注釋支持:遇到#時(shí)將該行剩余部分視為注釋忽略,類(lèi)似JavaScript的//。修改skipSpace函數(shù)使其跳過(guò)注釋(將注釋視為空白):
function skipSpace(string) {
let first = string.search(/\S/);
if (first == -1) return "";
if (string[first] == "#") {
// 跳過(guò)#直到行尾
let end = string.indexOf("\n", first);
if (end == -1) end = string.length;
return skipSpace(string.slice(end));
}
return string.slice(first);
}
測(cè)試用例:
console.log(parse("# hello\nx"));
// → {type: "word", name: "x"}
console.log(parse("a # one\n # two\n()"));
// → {type: "apply",
// operator: {type: "word", name: "a"},
// args: []}
作用域修復(fù)
目前只能通過(guò)define賦值,它同時(shí)用于定義新綁定和更新現(xiàn)有綁定,這種歧義會(huì)導(dǎo)致問(wèn)題:當(dāng)試圖更新非局部綁定時(shí),會(huì)在局部作用域創(chuàng)建同名新綁定。
添加set特殊形式,用于更新綁定值:若當(dāng)前作用域不存在該綁定,則在外層作用域查找;若完全未定義則拋出ReferenceError。
specialForms.set = (args, scope) => {
if (args.length != 2 || args[0].type != "word") {
throw new SyntaxError("set使用錯(cuò)誤");
}
let name = args[0].name;
let value = evaluate(args[1], scope);
let currentScope = scope;
// 查找綁定所在的作用域
while (currentScope) {
if (Object.hasOwn(currentScope, name)) {
currentScope[name] = value;
return value;
}
currentScope = Object.getPrototypeOf(currentScope);
}
throw new ReferenceError(`未定義的綁定: ${name}`);
};
測(cè)試用例:
run(`
do(define(x, 4),
define(setx, fun(val, set(x, val))),
setx(50),
print(x))
`);
// → 50
run(`set(quux, true)`);
// → ReferenceError: 未定義的綁定: quux