MyLang はPython
、 JavaScript
、およびC
からインスピレーションを得たシンプルな教育用プログラミング言語で、主に再帰降下パーサーを楽しく書いてインタプリタの世界を探索することを目的として、短時間で個人的な課題として書かれています。運用環境で使用できるライブラリとフレームワークを備えた本格的なスクリプト言語を期待しないでください。ただし、 MyLang
は最小限の組み込みセットがあり、実用的な目的にも使用できます。
MyLang はポータブルC++17 で書かれています。現時点では、プロジェクトには標準 C++ ライブラリ以外の依存関係はありません。 GNU make
がインストールされている場合は、次を実行してビルドします。
$ make -j
それ以外の場合は、すべての .cpp ファイルをコンパイラに渡し、 src/
ディレクトリをインクルード検索パスに追加します。依存関係がないことの最も優れた点の 1 つは、1 回限りのビルドにビルド システムが必要ないことです。
BUILD_DIR
オプションを渡してmake
だけです。
$ 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 に初期化するのではなく、3 つの変数すべてを値 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)
など) は const 評価されます。
現時点では、 MyLang
次の (組み込み) タイプのみをサポートしています。
None none
の型。Python のNone
に相当します。値が割り当てられていないで宣言されたばかりの変数は、値none
を持ちます (例var x;
)。戻り値を持たない関数にも同じことが当てはまります。また、失敗した場合に、 find()
などの組み込み関数によって特別な値として使用されます。
Integer符号付きポインタ サイズの整数 (例: 3
)。
Float浮動小数点数 (例: 1.23
)。内部的には、long double です。
文字列「hello」のような文字列。文字列は不変であり、スライスをサポートします (例: s[3:5]
、 s[3:]
、またはs[-2:]
、 Python
と同じ意味を持ちます)。
Array配列およびタプルの可変型 (例: [1,2,3]
)。異なるタイプの項目を含めることができ、書き込み可能なスライスをサポートします。配列スライスはコピーのように動作しますが、内部ではコピーオンライト技術を使用します。
辞書辞書は、 Python
の構文{"a": 3, "b": 4}
を使用して定義されたハッシュマップです。要素には、使い慣れた構文d["key-string"]
またはd[23]
を使用してアクセスし、 find()
で検索したり、 erase()
で削除したりできます。現時点では、文字列、整数、および浮動小数点のみが辞書のキーとして使用できます。特典: 識別子のような文字列キーには、「member of」構文d.key
を使用してアクセスすることもできます。
関数スタンドアロン関数とラムダはどちらも同じオブジェクト タイプを持ち、他のオブジェクトと同様に渡すことができます (以下を参照)。ただし、キャプチャ リストを持つことができるのはラムダだけです。通常の関数は const 評価中に実行できませんが、 pure
関数は実行できます。純粋な関数は const とその引数のみを参照できます。
例外スローできる唯一のオブジェクトのタイプ。これらを作成するには、 exception()
組み込み関数またはそのショートカットex()
を使用します。
条件文はC
とまったく同じように機能します。構文は次のとおりです。
if ( conditionExpr ) {
# Then block
} else {
# Else block
}
また、単一ステートメント ブロックの場合、 C
と同様に{ }
中括弧を省略できます。 conditionExpr
は任意の式を指定できます (例: (a=3)+b >= c && !d
。
conditionExpr
が const 評価可能な式の場合、if ステートメント全体が true 分岐に置き換えられ、false 分岐は破棄されます。たとえば、次のスクリプトについて考えてみましょう。
const a = 3 ;
const b = 4 ;
if ( a < b ) {
print ( " yes " ) ;
} else {
print ( " no " ) ;
}
常に「はい」と出力されるだけでなく、その前に何もチェックする必要さえありません。抽象構文ツリーを確認します。
$ ./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
では関数は通常のシンボルとして扱われ、この言語ではスタンドアロン関数とラムダの間に実質的な違いはありません。たとえば、次のようにしてadd
関数 (上記) をラムダとして宣言できます。
var add = func ( x , y ) => x + y ;
注: 式で関数オブジェクトを作成する場合、それらに名前を割り当てることはできません。
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
誰もが予想するように、キャプチャを含むラムダには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
通常のユーザー定義関数オブジェクト (ラムダを含む) は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 ] ) ;
純粋な関数はスタンドアロン関数として定義でき、非 const パラメーターとともに使用することもできます。したがって、関数が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
が定数ではないため実際の関数呼び出しが発生しますが、2 番目のケースではリテラル整数を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 " ) ;
}
注: const 評価中に定数式 (例: int("blah")
) によって例外が生成された場合、例外処理ロジックをバイパスして、エラーが直接報告されます。その理由は、早期失敗を強制するためです。
複数の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()
を使用して、ペイロード データとして1234
持つMyError
という例外オブジェクトを作成し、後でスローしました。その後、 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
が浮動小数点数の場合、2 番目のパラメータは出力文字列内の必要な 10 進桁数を示します。
int(value)
指定された文字列を整数に変換します。値が浮動小数点の場合は切り捨てられます。値が文字列の場合、可能であれば解析されて整数に変換されます。値がすでに整数の場合は、そのまま返されます。
float(value)
指定された値を浮動小数点数に変換します。値が整数の場合は、浮動小数点数に変換されます。値が文字列の場合、可能であれば解析され、float に変換されます。値がすでに float の場合は、そのまま返されます。
clone(obj)
指定されたオブジェクトのクローンを作成します。配列、辞書、キャプチャ付きラムダなどの重要なオブジェクトに便利です。
type(value)
指定された値の型の名前を文字列形式で返します。デバッグに役立ちます。
hash(value)
value
キーとして使用される場合、辞書が内部的に使用するハッシュ値を返します。現時点では、整数、浮動小数点、文字列のみがhash()
をサポートしています。
array(N)
none
の配列を返します