鸚鵡虛擬機器/Squaak 教程/作用域和子程式
第 1 集: 介紹
第 2 集: 深入編譯器內部
第 3 集: Squaak 細節和第一步
第 4 集: PAST 節點和更多語句
第 5 集: 變數宣告和作用域
第 6 集: 作用域和子程式
第 7 集: 運算子和優先順序
第 8 集: 雜湊表和陣列
第 9 集: 總結和結論
在 第 5 集 中,我們學習了變數宣告和作用域實現。我們當時涵蓋了大量資訊,但為了保持文章簡短,沒有講完整的故事。在本集裡,我們將介紹遺漏的部分,這也會導致子程式的實現。
在上一集裡,我們把區域性變數放入當前塊的符號表中。正如我們之前所見,使用 do-block 語句,作用域可以巢狀。考慮這個例子
do
var x = 42
do
print(x)
end
end
在這個例子中,print 語句應該列印 42,即使 x 沒有在引用它的作用域中宣告。編譯器是如何知道它仍然是一個區域性變數的呢?這很簡單:它應該在所有作用域中查詢,從最內層的開始。只有當在任何作用域中找到該變數時,才應該將它的作用域設定為“詞法”,以便生成正確的指令。
我想到的解決方案如下所示。請注意,我不能 100% 確定這是“最佳”解決方案,因為我對 PAST 編譯器的個人理解有限。因此,雖然這個解決方案有效,但我可能會教你錯誤的“習慣”。請注意這一點。
method identifier($/) {
our @?BLOCK;
my $name := ~$<ident>;
my $scope := 'package'; # default value
# go through all scopes and check if the symbol
# is registered as a local. If so, set scope to
# local.
for @?BLOCK {
if $_.symbol($name) {
$scope := 'lexical';
}
}
make PAST::Var.new( :name($name),
:scope($scope),
:viviself('Undef'),
:node($/) );
}
你可能之前注意到了 viviself 屬性。這個屬性會導致額外的指令,這些指令會在變數不存在時初始化它。如你所知,全域性變數在使用時會自動建立。我們之前提到過未初始化的變數的預設值為“Undef”:viviself 屬性就是做這個的。對於區域性變數,我們使用這種機制來設定(可選的)初始化值。當識別符號是一個引數時,如果該引數所屬的子程式被呼叫時沒有收到值,那麼該引數將被自動初始化。實際上,這意味著 Squaak 中的所有引數都是可選的!
我們之前已經提到過子程式,並介紹了 PAST::Block 節點型別。我們還簡要提到了可以在 PAST::Block 節點上設定的 blocktype 屬性,它指示該塊是立即執行(例如,do-block 或 if 語句)還是表示宣告(例如,子程式)。現在讓我們看看子程式定義的語法規則
rule sub_definition {
'sub' <identifier> <parameters>
<statement>*
'end'
{*}
}
rule parameters {
'(' [<identifier> [',' <identifier>]* ]? ')'
{*}
}
這相當直接,這些規則的操作方法也很簡單,你將會看到。然而,首先讓我們看看子定義的規則。為什麼子體被定義為 <statement>* 而不是 <block>?當然,子程式定義了一個新的作用域,這已經被 <block> 涵蓋了。好吧,你說得對。但是,正如我們將會看到的那樣,當建立一個新的 PAST::Block 節點時,我們已經太晚了!引數已經被解析,並且沒有被放入塊的符號表中。這是一個問題,因為引數很可能在子程式體中使用,而且由於它們沒有被註冊為區域性變數(它們是),所以對引數的任何使用都不會被編譯成獲取任何引數的正確指令。
那麼,我們如何以一種高效的方式解決這個問題呢?
解決方案很簡單。引數只存在於子程式體中,由一個 PAST::Block 節點表示。為什麼不在 parameters 規則的操作方法中建立 PAST::Block 節點呢?這樣做,塊就已經到位了,引數也會及時地被註冊為區域性符號。讓我們看看操作方法。
method parameters($/) {
our $?BLOCK;
our @?BLOCK;
my $past := PAST::Block.new( :blocktype('declaration'),
:node($/) );
# now add all parameters to this block
for $<identifier> {
my $param := $( $_ );
$param.scope('parameter');
$past.push($param);
# register the parameter as a local symbol
$past.symbol($param.name(), :scope('lexical'));
}
# now put the block into place on the scope stack
$?BLOCK := $past;
@?BLOCK.unshift($past);
make $past;
}
method sub_definition($/) {
our $?BLOCK;
our @?BLOCK;
my $past := $( $<parameters> );
my $name := $( $<identifier> );
# set the sub's name
$past.name( $name.name() );
# add all statements to the sub's body
for $<statement> {
$past.push( $( $_ ) );
}
# and remove the block from the scope
# stack and restore the current block
@?BLOCK.shift();
$?BLOCK := @?BLOCK[0];
make $past;
}
首先,讓我們看看 parameters 的解析操作。首先,建立一個新的 PAST::Block 節點。然後,我們遍歷識別符號列表(可以為空),每個識別符號代表一個引數。在獲取引數的結果物件(只是一個識別符號)後,我們將它的作用域設定為“parameter”,並將它新增到塊物件中。之後,我們以“lexical”的作用域將引數註冊為塊物件的符號。引數只是區域性變數的一種特殊型別,子程式中引數和宣告的區域性變數之間沒有區別,除了引數通常會用子程式呼叫時傳遞的值進行初始化。
處理完引數後,我們將當前塊(由我們的包變數 $?BLOCK 引用)設定為我們剛剛建立的 PAST::Block 節點,並將它推送到作用域棧(由我們的包變數 @?BLOCK 引用)上。
在整個子程式定義解析完後,會呼叫操作方法 sub_definition。這將獲取引數的結果物件,該物件是將代表子程式的 PAST::Block 節點。在獲取子程式名稱的結果物件後,我們將名稱設定在塊節點上,並將所有語句新增到塊中。之後,我們將這個塊節點從作用域棧(@?BLOCK)中彈出,並恢復當前塊($?BLOCK)。
很簡單,對吧?
定義了子程式後,你就會想要呼叫它。在 第 5 集 的練習中,我們已經提供了一些關於如何建立子程式呼叫的 PAST 節點的提示。在本節中,我們將提供完整的描述。首先,我們將介紹語法規則。
rule sub_call {
<primary> <arguments>
{*}
}
這不僅允許你透過名稱呼叫子程式,你還可以將子程式儲存在陣列或雜湊欄位中,然後從那裡呼叫它們。讓我們來看看操作方法,它非常直接。
method sub_call($/) {
my $invocant := $( $<primary> );
my $past := $( $<arguments> );
$past.unshift($invocant);
make $past;
}
method arguments($/) {
my $past := PAST::Op.new( :pasttype('call'), :node($/) );
for $<expression> {
$past.push( $( $_ ) );
}
make $past;
}
sub_call 方法的結果物件應該是一個 PAST::Op 節點(型別為 'call'),它包含多個子節點:第一個是呼叫者物件,所有剩餘的子節點都是對該子程式呼叫的引數。
為了將引數的結果物件“移動”到 sub_call 方法中,我們在方法引數中建立了 PAST::Op 節點,然後由 sub_call 獲取。在 sub_call 中,呼叫者物件被設定為第一個子節點(使用 unshift)。這很容易,不是嗎? :-)
在本集裡,我們完成了 Squaak 中作用域的實現,並實現了子程式。我們的語言進展順利!在下一集裡,我們將探索如何實現運算子和運算子優先順序表,以便高效地解析表示式。
與此同時,如果你遇到任何問題或疑問,請隨時留言!
- 問題 1
現在你應該對 Squaak 中作用域的實現有了一個很好的瞭解。我們還沒有實現 for 語句,因為它需要適當的作用域處理才能實現。實現這個。檢視第三集,瞭解定義 for 語句語法的 BNF 規則。在實現它的時候,你會遇到和我們實現子例程和引數時一樣的困難。使用相同的技巧來實現 for 語句。
- 解答
首先,讓我們看看 for 語句的 BNF
for-statement ::= 'for' for-init ',' expression [step]
'do'
block
'end'
step ::= ',' expression
for-init ::= 'var' identifier '=' expression
將其轉換為 Perl 6 規則非常容易
rule for_statement {
'for' <for_init> ',' <expression> <step>?
'do' <block> 'end'
{*}
}
rule step {
',' <expression>
{*}
}
rule for_init {
'var' <identifier> '=' <expression>
{*}
}
非常容易吧?讓我們看一下語義。for 迴圈只是編寫 while 迴圈的另一種方式,但在某些情況下更容易。這個
for var <ident> = <expr1>, <expr2>, <expr3> do
<block>
end
對應於
do
var <ident> = <expr1>
while <ident> <= <expr2> do
<block>
<ident> = <ident> + <expr3>
end
end
如果 <expr3> 缺失,則預設為值“1”。注意,步長表示式 (expr3) 應該是正數;迴圈條件包含 <= 運算子。當你指定一個負數步長表示式時,迴圈變數只會減少值,這永遠不會使迴圈條件為假(除非它溢位,但那是另一個問題;這甚至可能在 Parrot 中引發異常;我並不知道)。允許負數步長表示式會引入更多複雜性,我認為這對於本教程語言來說不值得。
注意,迴圈變數 <ident> 對 for 迴圈是本地的;這在等效的 while 迴圈中透過周圍的 do/end 對來表示:一個新的 do/end 對定義了一個新的(巢狀的)作用域;在 end 關鍵字之後,迴圈變數不再可見。
讓我們實現 for 語句的動作方法。正如練習說明中提到的,我們遇到了與子例程引數相同的情況。在這種情況下,我們正在處理對 for 語句是本地的迴圈變數。讓我們看看 for_init 的規則
method for_init($/) {
our $?BLOCK;
our @?BLOCK;
## create a new scope here, so that we can
## add the loop variable
## to this block here, which is convenient.
$?BLOCK := PAST::Block.new( :blocktype('immediate'),
:node($/) );
@?BLOCK.unshift($?BLOCK);
my $iter := $( $<identifier> );
## set a flag that this identifier is being declared
$iter.isdecl(1);
$iter.scope('lexical');
## the identifier is initialized with this expression
$iter.viviself( $( $<expression> ) );
## enter the loop variable into the symbol table.
$?BLOCK.symbol($iter.name(), :scope('lexical'));
make $iter;
}
所以,正如我們在引數動作方法中為子例程建立了一個新的 PAST::Block 一樣,我們在定義迴圈變數的動作方法中為 for 語句建立了一個新的 PAST::Block。(猜猜我們為什麼將 for-init 設定為一個子規則,而沒有在 for 語句規則中放入“var <ident> = <expression>”)。此塊是迴圈變數存在的場所。迴圈變數被宣告,使用 viviself 屬性進行初始化,並被輸入到新塊的符號表中。注意,在建立新的 PAST::Block 物件之後,我們將它放到堆疊作用域上。
現在,for 語句的動作方法很長,所以我只會嵌入我的註釋,這使得閱讀起來更容易。
method for_statement($/) {
our $?BLOCK;
our @?BLOCK;
首先,獲取 for 語句初始化規則的結果物件;這是 PAST::Var 物件,表示迴圈變數的宣告和初始化。
my $init := $( $<for_init> );
然後,為迴圈變數建立一個新節點。是的,另一個(除了當前包含在 PAST::Block 中的)。這個節點在迴圈體程式碼結束時(每次迭代)更新迴圈變數時使用。與另一個節點的不同之處在於,它沒有 isdecl 標誌,也沒有 viviself 子句,這會導致額外的指令檢查變數是否為空(我們知道它不是空的,因為我們初始化了迴圈變數)。
## cache the name of the loop variable
my $itername := $init.name();
my $iter := PAST::Var.new( :name($itername),
:scope('lexical'),
:node($/) );
現在,從作用域堆疊中檢索 PAST::Block 節點,並將所有語句 PAST 節點推入它。
## the body of the loop consists of the statements written by the user and
## the increment instruction of the loop iterator.
my $body := @?BLOCK.shift();
$?BLOCK := @?BLOCK[0];
for $<statement> {
$body.push($($_));
}
如果存在一個步長,我們使用該值;否則,我們使用預設步長“1”。
負數步長將不起作用,但如果你感覺幸運,你可以繼續嘗試。這並不難,只是很多工作,我現在太懶了……嗯,我的意思是,我把這作為習題留給讀者。
my $step;
if $<step> {
my $stepsize := $( $<step>[0] );
$step := PAST::Op.new( $iter, $stepsize, :pirop('add'), :node($/) );
}
else { ## default is increment by 1
$step := PAST::Op.new( $iter,
:pirop('inc'),
:node($/) );
}
迴圈變數的遞增是迴圈體的一部分,所以將遞增語句新增到 $body 中。
$body.push($step);
迴圈條件使用 <= 運算子,並將迴圈變數與指定的最大值進行比較。
## while loop iterator <= end-expression
my $cond := PAST::Op.new( $iter, $( $<expression> ),
:name('infix:<=') );
現在我們有了迴圈條件和迴圈體的 PAST,所以現在建立一個 PAST 來表示(while)迴圈。
my $loop := PAST::Op.new( $cond, $body,
:pasttype('while'),
:node($/) );
最後,迴圈變數的初始化應該在迴圈本身之前進行,所以建立一個 PAST::Stmts 節點來執行此操作
make PAST::Stmts.new( $init, $loop,
:node($/) );
}
哇,我們做到了!這是一個很好的例子,說明如何使用 PAST 實現非平凡的語句型別。