MyLang 是一種簡單的教育程式語言,受到Python
、 JavaScript
和C
啟發,作為個人挑戰而編寫,主要是為了在短時間內享受編寫遞歸下降解析器的樂趣並探索解釋器的世界。不要指望一個成熟的腳本語言具有可供生產使用的函式庫和框架。然而, MyLang
有一個最小的內建函數集,它也可以用於實際目的。
MyLang 是用可移植的C++17 編寫的:目前,該專案除了標準 C++ 函式庫之外沒有任何依賴項。要建置它,如果您安裝了GNU make
,只需運行:
$ make -j
否則,只需將所有 .cpp 檔案傳遞給編譯器並將src/
目錄新增到包含搜尋路徑中。沒有依賴性的最好的事情之一是不需要一次性構建的構建系統。
make
傳遞BUILD_DIR
選項:
$ make -j BUILD_DIR=other_build_directory
如果您也想執行 MyLang 的測試,則只需使用 TESTS=1 進行編譯並使用 OPT=0 停用最佳化,以獲得更好的偵錯體驗:
$ make -j TESTS=1 OPT=0
然後,執行所有測試:
$ ./build/mylang -rt
值得注意的是,雖然像 GoogleTest 和 Boost.Test 這樣的測試框架比我們在src/tests.cpp
中的簡單測試引擎強大且靈活得多,但它們是外部依賴項。依賴性越少越好,對嗎? :-)
描述MyLang
最簡單的說法是:一種看起來像 C 的動態 python 語言。也許,學習這種語言的最快方法是查看samples/
目錄中的腳本,同時查看下面的簡短文件。
MyLang
是一種動態鴨子類型語言,就像Python
一樣。如果您了解Python
並且願意使用{ }
大括號,您將自動能夠使用它。沒有什麼驚喜。字串是不可變的,就像在Python
中一樣,陣列可以使用[ ]
定義,就像在Python
中一樣,字典也可以使用{ }
定義。該語言也支援使用與Python
相同的[start:end]
語法的陣列切片。
據說, MyLang
在幾個方面與Python
和其他腳本語言不同:
支援使用const
聲明的解析時常數。
所有變數都必須使用var
宣告。
變數有一個作用域,就像C
一樣。當在巢狀區塊中使用var
明確重新聲明變數時,支援遮蔽。
所有表達式語句必須以;
結尾就像C
、 C++
和Java
中的那樣。
存在關鍵字true
和false
,但沒有boolean
類型。就像在 C 中一樣, 0
是 false,其他一切都是true
。然而,字串、陣列和字典都有一個布林值,就像在Python
中一樣(例如,空數組被認為是false
)。 true
內建函數只是整數1
的別名。
賦值運算子=
可以像在C
中一樣在表達式內部使用,但由於數組擴充功能,沒有逗號運算子之類的東西。
MyLang 支援經典的for
迴圈和顯式的foreach
迴圈。
MyLang 目前不支援自訂類型。然而,字典支援一個很好的語法糖:除了主要語法d["key"]
之外,對於字串鍵,還支援語法d.key
。
變數始終使用var
聲明,並存在於聲明的作用域中(同時在嵌套作用域中可見)。例如:
# Variable declared in the global scope
var a = 42 ;
{
var b = 12 ;
# Here we can see both `a` and `b`
print ( a , b ) ;
}
# But here we cannot see `b` .
可以使用以下熟悉的語法聲明多個變數:
var a , b , c ;
但有一個警告,可能是MyLang
唯一「令人驚訝」的功能:初始化變數不像 C 那樣運作。
var a , b , c = 42 ;
在本例中,我們不是僅僅聲明a
和b
並將c
初始化為 42,而是將所有三個變數初始化為值 42。
var a , b , c = [ 1 , 2 , 3 ] ;
常數的宣告方式與變數類似,但不能在嵌套作用域中隱藏。例如:
const c = 42 ;
{
# That's not allowed
const c = 1 ;
# That's not allowed as well
var c = 99 ;
}
在MyLang
中,常數在解析時求值,其方式與C++
的constexpr宣告類似(但我們討論的是編譯時)。初始化const
時,除了整套const內建函數之外,還可以使用任何類型的文字。例如:
const val = sum ( [ 1 , 2 , 3 ] ) ;
const x = " hello " + " world " + " " + join ( [ " a " , " b " , " c " ] , " , " ) ;
若要了解如何準確地評估常數,請使用-s
選項執行解釋器,以在執行腳本之前轉儲抽象語法樹。對於上面的例子:
$ cat > t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
$ ./build/mylang t
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
)
--------------------------
驚訝嗎?那麼,除了陣列和字典之外的常數甚至不會被實例化為變數。它們只是在運行時不存在。讓我們使用x
加入一條語句:
$ cat >> t
print(x);
$ cat t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
print(x);
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
CallExpr(
Id("print")
ExprList(
"hello world a,b,c"
)
)
)
--------------------------
hello world a,b,c
現在,一切都應該有意義了。數組和字典也會發生幾乎相同的情況,只是後者也在運行時進行實例化,以避免到處都有潛在的巨大文字。考慮以下範例:
$ ./build/mylang -s -e 'const ar=range(4); const s=ar[2:]; print(ar, s, s[0]);'
Syntax tree
--------------------------
Block(
ConstDecl(
Id("ar")
Op '='
LiteralArray(
Int(0)
Int(1)
Int(2)
Int(3)
)
)
ConstDecl(
Id("s")
Op '='
LiteralArray(
Int(2)
Int(3)
)
)
CallExpr(
Id("print")
ExprList(
Id("ar")
Id("s")
Int(2)
)
)
)
--------------------------
[0, 1, 2, 3] [2, 3] 2
正如您所看到的,在初始化常數s
,切片操作已在解析時進行了評估,但這兩個數組也在運行時存在。相反,const 表達式上的下標操作會轉換為文字。這看起來是一個很好的效能權衡:整數、浮點數和字串等小值在const 求值期間被轉換為文字,而陣列和字典(可能很大)在運行時保留為唯讀符號,但仍然允許對它們的一些操作(例如[index]
和len(arr)
)要進行常數評估。
MyLang
目前僅支援以下(內建)類型:
None none
的類型,相當於Python 的None
。剛剛宣告的變數沒有分配值, none
值(例如var x;
)。這同樣適用於沒有傳回值的函數。此外,它也被find()
等內建函數用作特殊值,以防失敗。
Integer有符號指標大小的整數(例如3
)。
Float浮點數(例如1.23
)。在內部,它是一個長雙精度。
字串類似“hello”的字串。字串是不可變的並且支援切片(例如s[3:5]
或s[3:]
或s[-2:]
,與Python
中具有相同的含義)。
數組數組和元組的可變類型(例如[1,2,3]
)。它可以包含不同類型的項目,並且支援可寫切片。數組切片的行為類似於副本,但在幕後,它們使用寫入時複製技術。
字典字典是使用Python
語法定義的雜湊映射: {"a": 3, "b": 4}
。使用熟悉的語法d["key-string"]
或d[23]
存取元素,可以使用find()
尋找並使用erase()
刪除。目前,只有字串、整數和浮點數可以用作字典的鍵。 Perks :類似識別碼的字串鍵也可以使用「member of」語法存取: d.key
。
函數獨立函數和 lambda 都具有相同的物件類型,並且可以像任何其他物件一樣傳遞(見下文)。但是,只有 lambda 可以有捕獲清單。常規函數無法在 const 求值期間執行,而pure
函數可以。純函數只能看到常數及其參數。
Exception唯一可以拋出的物件類型。若要建立它們,請使用內建的exception()
或其捷徑ex()
。
條件語句的工作方式與C
中的完全相同。語法是:
if ( conditionExpr ) {
# Then block
} else {
# Else block
}
對於單語句塊,可以像C
中一樣省略{ }
大括號。 conditionExpr
可以是任何表達式,例如: (a=3)+b >= c && !d
。
當conditionExpr
是可以常數求值的表達式時,整個if語句將會被true分支替換,而false分支將會被丟棄。例如,考慮以下腳本:
const a = 3 ;
const b = 4 ;
if ( a < b ) {
print ( " yes " ) ;
} else {
print ( " no " ) ;
}
它不僅總是列印“yes”,而且在執行此操作之前甚至不需要檢查任何內容。檢查抽象語法樹:
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
Block(
CallExpr(
Id("print")
ExprList(
"yes"
)
)
)
)
--------------------------
yes
MyLang
支援經典的while
和for
迴圈。
while ( condition ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
for ( var i = 0 ; i < 10 ; i += 1 ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
這裡,像上面的情況一樣,可以省略{ }
大括號。與C
相比,只有一些差異值得指出:
目前MyLang
中不存在++
和--
運算子。
若要宣告多個變量,請使用語法: var a, b = [3,4];
或只是var a,b,c,d = 0;
如果您希望所有變數具有相同的初始值。
若要增加多個變數的值,請使用語法: a, b += [1, 2]
。在極為罕見且複雜的情況下,在 for 迴圈的增量語句中,我們需要使用不同的表達式為每個變數分配一個新變量,請利用賦值中的擴展語法: i, j = [i+2, my_next(i, j*3)]
。
MyLang
使用非常熟悉的語法來支援foreach
循環:
var arr = [ 1 , 2 , 3 ] ;
foreach ( var e in arr ) {
print ( " elem: " , e ) ;
}
Foreach 迴圈可用於陣列、字串和字典。例如,迭代字典中的每個<key, value>
對很簡單:
var d = { " a " : 3 , " b " : 10 , " c " : 42 } ;
foreach ( var k , v in d ) {
print ( k + " => " + str ( v ) ) ;
}
要僅迭代每個鍵,只需var k in d
即可。
MyLang
也支援 foreach 迴圈中的枚舉。檢查以下範例:
var arr = [ " a " , " b " , " c " ] ;
foreach ( var i , elem in indexed arr ) {
print ( " elem[ " + str ( i ) + " ] = " + elem ) ;
}
換句話說,當容器名稱前面帶有關鍵字indexed
時,第一個變數在每次迭代時都會分配一個漸進的數字。
在迭代小型固定大小數組的陣列時(考慮元組),可以在 foreach 迴圈中直接擴展這些「元組」:
var arr = [
[ " hello " , 42 ] ,
[ " world " , 11 ]
] ;
foreach ( var name , value in arr ) {
print ( name , value ) ;
}
# This is a shortcut for :
foreach ( var elem in arr ) {
# regular array expansion
var name , value = elem ;
print ( name , value ) ;
}
# Which is a shortcut for :
foreach ( var elem in arr ) {
var name = elem [ 0 ] ;
var value = elem [ 1 ] ;
print ( name , value ) ;
}
聲明一個函數很簡單:
func add ( x , y ) {
return x + y ;
}
但也支援一些快捷方式。例如,對於像上面這樣的單語句函數,可以使用以下語法:
func add ( x , y ) => x + y ;
此外,雖然始終為無參數函數編寫()
是一個好習慣,但它們在這種語言中實際上是可選的:
func do_something { print ( " hello " ) ; }
在MyLang
中,函數被視為常規符號,且該語言中的獨立函數和 lambda 之間沒有實質差異。例如,我們可以將上面的add
函數宣告為 lambda:
var add = func ( x , y ) => x + y ;
注意:在表達式中建立函數物件時,我們不允許為它們指定名稱。
Lambda 也支援捕獲列表,但為了確保清晰度,不支援隱式捕獲。當然,lambda 可以像任何其他物件一樣返回。例如:
func create_adder_func ( val ) =>
func [ val ] ( x ) => x + val ;
var f = create_adder_func ( 5 ) ;
print ( f ( 1 ) ) ; # Will print 6
print ( f ( 10 ) ) ; # Will print 15
正如任何人所期望的那樣,帶有捕獲的 Lambda 函數有一個state 。考慮以下腳本:
func gen_counter ( val ) => func [ val ] {
val += 1 ;
return val ;
} ;
var c1 = gen_counter ( 5 ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
# Clone the `c1` lambda object as `c2` : now it will have
# its own state , indipendent from `c1` .
var c2 = clone ( c1 ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c2: " , c2 ( ) ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
它產生輸出:
c1: 6
c1: 7
c1: 8
c2: 9
c2: 10
c2: 11
c1: 9
c1: 10
c1: 11
常規使用者定義函數物件(包括 lambda)不被視為const
,因此不能在 const 求值期間運作。這是一個相當強的限制。考慮以下範例:
const people = [
[ " jack " , 3 ] ,
[ " alice " , 11 ] ,
[ " mario " , 42 ] ,
[ " bob " , 38 ]
] ;
const sorted_people = sort ( people , func ( a , y ) => a [ 0 ] < b [ 0 ] ) ;
在這種情況下,腳本無法建立sorted_people
const 陣列。因為我們將函數物件傳遞給 const sort()
內建函數,所以我們將會收到ExpressionIsNotConstEx
錯誤。當然,如果sorted_people
被聲明為var
,腳本將會運行,但是數組將不再是 const ,並且我們將無法從任何解析時優化中受益。因此,雖然sort()
內建函數可以在 const 求值期間調用,但當它具有自訂compare func
參數時,這就不再可能了。
為了克服剛才所描述的限制, MyLang
有一個特殊的純函數語法。當一個函數在func
之前使用pure
關鍵字聲明時,解釋器會以一種特殊的方式對待它:它可以在 const 求值期間和運行時隨時調用,但該函數不能看到全局變量,也不能捕獲任何內容:它只能使用常數及其參數的值:這正是我們在 const 求值過程中所需要的。例如,要在 const 求值期間產生sorted_people
,只需寫:
const sorted_people = sort ( people , pure func ( a , b ) => a [ 0 ] < b [ 0 ] ) ;
純函數可以定義為獨立函數,也可以與非常量參數一起使用。因此,如果一個函數可以聲明為pure
,那麼它應該始終以這種方式聲明。例如,考慮以下腳本:
pure func add2 ( x ) => x + 2 ;
var non_const = 25 ;
print ( add2 ( non_const ) ) ;
print ( add2 ( 5 ) ) ;
語言引擎在運行時使用的抽象語法樹將是:
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
FuncDeclStmt(
Id("add2")
<NoCaptures>
IdList(
Id("x")
)
Expr04(
Id("x")
Op '+'
Int(2)
)
)
VarDecl(
Id("non_const")
Op '='
Int(25)
)
CallExpr(
Id("print")
ExprList(
CallExpr(
Id("add2")
ExprList(
Id("non_const")
)
)
)
)
CallExpr(
Id("print")
ExprList(
Int(7)
)
)
)
--------------------------
27
7
正如您所看到的,在第一種情況下,會發生實際的函數調用,因為non_const
不是常數,而在第二種情況下,就像我們將文字整數傳遞給print()
一樣。
與其他構造一樣, MyLang
具有類似Python
的異常處理,但使用類似C++
語法。基本結構是try-catch
語句。讓我們來看一個例子:
try {
var input_str = " blah " ;
var a = int ( input_str ) ;
} catch ( TypeErrorEx ) {
print ( " Cannot convert the string to integer " ) ;
}
注意:如果常數表達式(例如int("blah")
)產生異常,則在 const 求值過程中,將直接報告錯誤,繞過任何異常處理邏輯。原因是為了強制實施早期失敗。
也允許使用多個catch
語句:
try {
# body
} catch ( TypeErrorEx ) {
# error handling
} catch ( DivisionByZeroEx ) {
# error handling
}
而且,如果可以使用相同的程式碼處理多個異常,也可以使用更短的語法:
try {
# body
} catch ( TypeErrorEx , DivisionByZeroEx as e ) {
# error handling
print ( e ) ;
} catch ( OutOfBoundsEx ) {
# error handling
}
異常可能包含數據,但目前沒有任何內建異常包含數據。可以使用try-catch
區塊擷取的內建運行時異常清單是:
相反,無法捕捉SyntaxErrorEx
等其他異常。在MyLang
中也可以使用 catch-anything 區塊捕獲任何異常:
try {
# body
} catch {
# Something went wrong .
}
目前,該語言不支援自訂類型。因此,不可能像其他語言那樣拋出任何類型的物件。要引發異常,需要使用特殊的內建函數exception()
或其捷徑ex()
。考慮以下範例:
try {
throw ex ( " MyError " , 1234 ) ;
} catch ( MyError as e ) {
print ( " Got MyError, data: " , exdata ( e ) ) ;
}
如直覺所示,我們使用ex()
創建並隨後拋出了一個名為MyError
異常對象,其中包含1234
作為負載資料。後來,在catch
區塊中,我們捕獲了異常,並使用內建的exdata()
提取了有效負載資料。
如果給定的例外不需要有效負載,則可以將ex()
的結果保存在變數中,然後使用可能更令人愉快的語法拋出它:
var MyError = ex ( " MyError " ) ;
throw MyError ;
MyLang
支援使用專用的rethrow
關鍵字在 catch 語句主體中重新引發例外:
try {
do_something ( ) ;
} catch {
print ( " Something went wrong!! " ) ;
rethrow ;
}
在某些情況下,在執行可能引發異常的程式碼區塊後,可能需要進行一些清理。對於這些情況, MyLang
支援眾所周知的finally
子句,其運作方式與C#
中完全相同:
try {
step1_might_throw ( ) ;
step2_might_throw ( ) ;
step3_might_throw ( ) ;
step4_might_throw ( ) ;
} catch ( TypeErrorEx ) {
# some error handling
} finally {
# clean - up
}
值得注意的是, try-finally
結構(沒有任何catch
子句)也是允許的。
當 const 參數傳遞給以下內建函數時,將在解析時對其進行求值。
defined(symbol)
檢查symbol
是否已定義。如果定義了符號,則傳回 1,否則傳回 0。
len(container)
傳回給定容器中的元素數量。
str(value, [decimal_digits])
將給定值轉換為字串。如果value
是浮點數,則第二個參數指示輸出字串中所需的小數位數。
int(value)
將給定字串轉換為整數。如果該值是浮點數,則會被截斷。如果該值是字串,則將對其進行解析並轉換為整數(如果可能)。如果該值已經是整數,則將按原樣傳回。
float(value)
將給定值轉換為浮點數。如果該值為整數,則會將其轉換為浮點數。如果該值是字串,如果可能的話,它將被解析並轉換為浮點型。如果該值已經是浮點數,它將按原樣返回。
clone(obj)
克隆給定的對象。對於數組、字典和捕獲的 lambda 等非平凡物件很有用。
type(value)
以字串形式傳回給定值的類型名稱。對於調試很有用。
hash(value)
當value
用作 key 時,傳回字典內部使用的雜湊值。目前,只有整數、浮點數和字串支援hash()
。
array(N)
傳回一個none
數組