このドキュメントは、ReoScript を使ってスクリプトを書く人と、.NET アプリケーションへ組み込む人の両方に向けた利用ガイドです。内容は現在の Source/ReoScript.sln とテストコードに基づいています。
ReoScript は .NET アプリケーションへ組み込むための ECMAScript 風スクリプトエンジンです。文法は JavaScript に近い一方で、.NET オブジェクト直接操作、モジュール読み込み、オブジェクト結合、タグ構文など、独自拡張も含みます。
主な特徴:
- JavaScript に近い構文でスクリプトを記述できる
ScriptRunningMachineから文字列、ファイル、式を実行できる- C# 側のオブジェクトや関数をグローバル変数として公開できる
- 設定次第で .NET 型の import、プロパティ操作、イベント接続ができる
- ラムダ式、モジュール、JSON、非同期タイマー、クロージャをサポートする
ReoScript 本体は Source/ReoScript/、CLI ランナーは Source/ReoScriptRunner/ にあります。
dotnet build Source/ReoScript.slnCLI ランナーの実行例:
dotnet run --project Source/ReoScriptRunner -- sample.reo
dotnet run --project Source/ReoScriptRunner -- -e "console.log(1 + 2)"
dotnet run --project Source/ReoScriptRunner -- -consoleReoScriptRunner の主なオプション:
-e <script>/-exec <script>: スクリプト文字列を直接実行-workpath <path>: 作業ディレクトリを設定-debug: デバッグモードを有効化-console: 実行後に対話コンソールへ入る-com: コンパイルモード。現行版では未実装
対話コンソールでは次が使えます。
?expr: 式を評価して表示?: 現在のグローバルオブジェクトを表示.path: スクリプトファイルをロード/quit,/q: 終了
var total = 0;
for (var i = 1; i <= 10; i++) {
total += i;
}
console.log(total);Run の戻り値は最後に評価された式の値です。
var a = 10
var b = 20
a + bこのスクリプト全体の戻り値は 30 になります。
var a = 10;
var b = 20, c = a + b;- 変数宣言は
var - グローバルスコープで宣言した
varはグローバル変数になる - 未設定値や存在しない値は
nullとして扱われる場面が多い
var n = 3.14;
var s = "hello";
var b = true;
var x = null;代表的な演算子:
- 算術:
+,-,*,/,% - 代入:
=,+=,-=,*=,/= - 比較:
==,!=,===,!==,<,<=,>,>= - 論理:
&&,||,! - インクリメント/デクリメント:
++,-- - ビット演算:
|,&,<<,>> - 三項演算子:
cond ? a : b - 型判定:
typeof,instanceof - プロパティ削除:
delete obj.key
var a = 10;
a += 5;
a++;
var label = a > 10 ? "large" : "small";ReoScript は自動セミコロン挿入をサポートします。
var a = 10
var b = 20
a + bただし for (...) ヘッダ内の区切りセミコロンは省略できません。
for (var i = 0; i < 5; i++) {
console.log(i);
}if (score >= 80) {
grade = "A";
} else {
grade = "B";
}var count = 5;
while (count) {
console.log(count);
count--;
}var sum = 0;
for (var i = 0; i < 10; i++) {
sum += i;
}switch (kind) {
case "error":
console.log("error");
break;
case "warn":
console.log("warn");
break;
default:
console.log("info");
break;
}for ... in は対象によって振る舞いが異なります。
- オブジェクト: プロパティ名を列挙
- 配列: 要素を列挙
for (key in obj) {
console.log(key);
}
for (item in arr) {
console.log(item);
}JavaScript のように配列インデックスを得る構文ではない点に注意してください。
function add(a, b) {
return a + b;
}関数宣言は呼び出しより後ろに書いても使えます。
var x = add(10, 20);
function add(a, b) {
return a + b;
}var add = function(a, b) {
return a + b;
};var add = (a, b) => a + b;
var square = x => x * x;
var total = ((a, b) => a + b)(2, 3);ブロック本体も使えます。
var classify = n => {
if (n >= 0) return "positive";
return "negative";
};ReoScript の関数はレキシカルスコープを持ちます。外側関数のローカル変数を内側関数が捕捉できます。
function makeCounter() {
var count = 0;
return function() {
count = count + 1;
return count;
};
}
var c1 = makeCounter();
var c2 = makeCounter();c1 と c2 は独立した状態を保持します。
関数には call と apply があり、呼び出し時の this を差し替えられます。
function bracketMe() {
return "[" + this + "]";
}
bracketMe.call("abc");var user = {
name: "alice",
age: 20
};アクセス方法:
user.name
user["name"]user.email = "a@example.com";
delete user.email;function User(name) {
this.name = name;
}
var u = new User("alice");new の後ろに初期化ブロックを付ける独自構文も使えます。
function User() {
this.role = "guest";
}
var u = new User() {
name: "alice",
active: true
};ReoScript にはオブジェクト同士を + で結合する拡張があります。
var a = { x: 10 };
var b = { y: 20 };
var c = a + b;var a = 1, b = 2;
var obj = { a, b };
var merged = { ...obj, c: 3 };
var { a, c } = merged;var arr = [1, 2, 3];
arr.push(4);
arr[0] = 10;標準の配列操作に加えて、組み込みスクリプトでいくつかの補助メソッドが定義されています。
pushspliceindexOfjoinconcatmapreducewherefirstlastequalseach
var arr = [1, 2, 3, 4, 5];
var even = arr.where(n => Math.floor(n / 2) == n / 2);
var doubled = arr.map(n => n * 2);
var total = arr.reduce((a, c) => a + c, 0);console が有効な環境では次が使えます。
console.read()console.readline()console.write(value)console.log(value)
主な定数と関数:
Math.PIMath.EMath.LN2Math.LN10Math.min(...)Math.max(...)Math.floor(...)Math.sqrt(...)Math.atan2(...)
var now = Date.now();
var d = new Date();
var ms = d.getTime();var obj = JSON.parse("{name:'apple',count:10}");
var str = JSON.stringify(obj);JSON.parse と JSON.stringify は変換関数も受け取れます。
var obj = JSON.parse(src, (key, value) => value);
var str = JSON.stringify(obj, (key, value) => String(value));var value = eval("1 + 2");eval は現在のスコープ内で評価されます。
var n = parseInt("FF", 16);debug オブジェクトは new ScriptDebugger(srm) を付けた場合に利用できます。
debug.assert(1 + 1 == 2);
var sw = debug.Stopwatch.startNew();ReoScript の条件式は truthy / falsy で評価されます。
Falsy:
nullfalse0NaN""
Truthy:
- 非 0 数値
- 非空文字列
- オブジェクト
- 配列
- 関数
var name = input || "guest";
var enabled = config && config.flag;&& と || は真偽値ではなく実際の値を返します。
try {
dangerous();
} catch (e) {
console.log(e.message);
} finally {
console.log("cleanup");
}catch は変数名なしでも書けます。
try {
dangerous();
} catch {
console.log("error");
}throw new Error("something wrong");
throw 10;Error オブジェクトでは message を参照できます。
import "common.reo";これは読み込んだスクリプトをグローバルスコープで実行します。
import "math.reo" as math;
var result = math.add(10, 3);こちらは別スコープでロードし、指定名へ束縛します。
var math = importModule("math.reo");
var result = math.add(3, 4);特徴:
- モジュールスコープはグローバルから分離される
- モジュール内の関数・変数が戻り値オブジェクトに公開される
- 同じファイルはキャッシュされ、原則 1 回だけ実行される
タイマー関数:
setTimeout(callback, milliseconds)setInterval(callback, milliseconds)clearInterval(id)
var count = 0;
var id = setInterval(function() {
count++;
if (count >= 5) {
clearInterval(id);
}
}, 100);イベントハンドラや非同期コールバック内の例外は、ホスト側で ScriptError イベントを購読して処理できます。
using unvell.ReoScript;
var srm = new ScriptRunningMachine();
srm.Run("var x = 10; var y = 20;");
var result = srm.CalcExpression("x + y");using unvell.ReoScript;
var srm = new ScriptRunningMachine();
srm.SetGlobalVariable("appName", "Demo");
srm.SetGlobalVariable("version", 1);
srm.Run("console.log(appName);");インデクサでも設定できます。
srm["answer"] = 42;using unvell.ReoScript;
var srm = new ScriptRunningMachine();
srm["hello"] = new NativeFunctionObject("hello", (ctx, owner, args) =>
{
return "Hello " + (args.Length > 0 ? args[0] : "world");
});using unvell.ReoScript;
var srm = new ScriptRunningMachine();
srm.WorkMode |= MachineWorkMode.AllowDirectAccess;
srm.SetGlobalVariable("user", new User());スクリプト側:
user.Nickname = "alice";
user.Hello();srm.WorkMode |= MachineWorkMode.AllowDirectAccess;
srm.ImportType(typeof(System.Windows.Forms.LinkLabel));srm.WorkMode |= MachineWorkMode.AllowDirectAccess
| MachineWorkMode.AllowImportTypeInScript
| MachineWorkMode.AllowCLREventBind;スクリプト側:
import System.Windows.Forms.*;
import System.Drawing.Point;
var f = new Form() {
text: "Form created in ReoScript"
};AllowCLREventBind を有効にすると、.NET イベントへ関数を割り当てられます。
var link = new LinkLabel() {
text: "click me",
click: function() {
f.close();
}
};using unvell.ReoScript;
using unvell.ReoScript.Diagnostics;
var srm = new ScriptRunningMachine();
var debugger = new ScriptDebugger(srm);これで debug.assert などが使えます。
無限ループ対策として、1 ループあたりの最大反復回数を設定できます。
srm.MaxIterationsPerLoop = 10_000_000;- 既定値は
10_000_000 0を指定すると無効化- 超過時は
ScriptExecutionTimeoutException
相対パス import の基準は WorkPath です。
srm.WorkPath = @"C:\scripts";HTML 風タグをオブジェクト生成へ使う独自構文があります。
function User() { }
var usr = <User />;文法上は次のようなテンプレート定義構文があります。
template<UserCard>(name, age) <User />;この系統は ReoScript 独自拡張で、一般的な JavaScript / JSX とは別物です。利用時は対象ホスト側の実装も確認してください。
ReoScript は JavaScript 互換ではなく ECMAScript 風言語です。特に次を意識してください。
for ... inで配列を回すとインデックスではなく要素が得られるundefinedはnullと近い扱いになる場面があるNaN == NaNが真になるテストケースがある。JavaScript と同じ挙動ではないdebugオブジェクトは標準では常に存在するわけではない- オブジェクト結合
a + b、new Type() { ... }、タグ構文など独自拡張がある
移植時は JavaScript の常識をそのまま当てはめず、必ず ReoScript 上で動作確認してください。
var config = {
title: "My App",
retryCount: 3,
endpoints: ["a", "b", "c"]
};function canPurchase(user, total) {
return user != null && user.active && total > 0;
}import System.Windows.Forms.*;
var button = new Button() {
text: "Run",
click: function() {
console.log("clicked");
}
};- CLI ランナー:
Source/ReoScriptRunner/ - 組み込みサンプル:
Samples/ - 言語テスト:
Source/TestCase/tests/ - コア実装:
Source/ReoScript/ScriptRunningMachine.cs
利用例を増やしたい場合は、まず Source/TestCase/tests/ の XML テストと Samples/ のサンプルを読むのが最短です。