概要
以前考えたシナリオスクリプト言語をもうちょっと真面目に考え直してみた。
目的
Lua言語の仕様に違反しない範囲で見通しの良いテキスト中心のスクリプト言語
見た目
return {
"吾輩は猫である。名前はまだない。",
"そんな吾輩であるが、数日前にマタタビを追いかけて走り回っていたらトラックに轢かれてしまった。",
{"GOTO", "JUMP"},
"これは本当にたまたまだったのだが、運が悪かったというかなんというか…………。",
{"LABEL", "JUMP"},
"何にせよその結果、中世ヨーロッパのような世界に転生してしまったのである。",
{{
"現代日本で生まれ育ち、1年間生きてきた記憶と人格を持ったままこのファンタジー世界に投げ込まれたのだ。",
"いや、べつに不満をぶちまけたいわけではない。ただ、ちょっとした偶然で死んでしまった結果がこれでは少しばかり納得がいかなかっただけだ。",
"しかも、誇り高き猫族だった吾輩にはこの人間の身体は窮屈すぎる。",
"動きも鈍いし、人間どもと一緒にいるだけでストレスが溜まるわ、毛もないから夏でも暑いわ…………etc,etc. ",
"それでも我慢していればそのうち慣れるだろうとたかを括っていたが、やはり無理があったようだ。",
"とにかく、この世界で生きるにあたって自分の名前が必要らしいことは理解していた。",
}}
}
ちなみに文章はAIのべりすとで作りました。
仕様
return { ~ }
はスクリプトをrequireやloadfile関数の戻り値としてそのままテーブルとして受け取れるようにするためのおまじないです。
"吾輩は猫である。名前はまだない。",
はテキストです。ホストのプログラムによって文章として画面に表示されることが期待される部分です。
{"LABEL", "JUMP"},
は命令です。同名のLua関数を呼び出して実行します。インタプリタのLua関数をホストプログラムに合わせて実装・書き換えすることでスクリプトからホストプログラムを制御します。
テキスト・命令を要素ごとにカンマ(,)で区切ります。
{{ ~ }}
はブロックです、テキスト・命令・ブロックをカンマで区切った集まりを入れ子にすることができます。
実際はテキストは{"TEXT", "吾輩は猫である"}
といったTEXT命令、
ブロックは{"BLOCK", {"現代日本で生まれ育ち", "1年間生きてきた記憶と人格を持った"}}
といったBLOCK命令
の省略形なのですべて命令(とその省略形)をカンマで区切った集まりとも言えます。
Lua関数の直接定義とかいらないと思うのでできないようにした
命令
命令はLua関数の呼び出しとして処理されます。命令の名前と引数をカンマで区切ります。
引数を""で囲むかどうかは命令によりますが、""で囲んで文字列として渡す命令がほとんどです。
{"命令", "引数1", "引数2", "引数3"...}
基本命令
ベースとなるインタプリタはホストプログラムに依存する関数は定義できないので、
簡単な制御構造、フラグ、スクリプトファイルの読み込みといったホストプログラムに依存しない命令が定義されています
- LABEL命令 - 引数に設定した名前でGOTO命令の飛び先を設定します。
- GOTO命令 - LABEL命令で設定した名前の飛び先に移動します。ブロックをまたいだジャンプはできません。
- TEXT命令 - 引数に設定されたテキストを表示します。
- BLOCK命令 - 引数に設定されたブロックを実行します。
- TABLE命令 - SET/UNSET命令で設定するフラグを保管するテーブル変数を設定します。
- SET命令 - 引数に設定した名前のフラグをtrueに設定します。
- UNSET命令 - 引数に設定した名前のフラグをfalseに設定します。
- IF命令 - 引数1に設定した名前のフラグがtrueなら引数2の、falseなら引数3に設定した名前の飛び先にジャンプします。設定していなければそのまま次の行に進みます。
- LOAD命令 - 引数に指定したスクリプトファイルを読み込んでブロックとして実行します。
- BREAK命令 - ブロックの実行を終了して呼び出し元のブロックに戻ります。
- END命令 - スクリプトの実行を終了します。BREAK命令と違いどのブロックで実行されても全体が終了します。
- ABORT命令 - 異常終了という形でスクリプトの実行を終了します。引数でメッセージを設定できます。
- WARNING命令 - 引数に設定した警告メッセージを設定します。
拡張命令
関数を上書きすることでインタプリタの動作をカスタマイズできるように特定のタイミングで呼ばれる関数(命令)が設定されています。
パフォーマンスの関係でオーバーライドはしない仕様にしたので、
上書きする場合は関数内のどこかでベースインタプリタ用の別名の関数(内部命令)を呼び出す必要があります。
- INIT命令 - スクリプトをインタプリタで実行した時に呼ばれる命令です。ベースの初期化のためにINITSCRIPT命令を呼び出す必要があります。INITSCRIPT命令の戻り値でINIT命令実行済みかどうかの判別ができます。
- TEXT命令 - テキスト及びTEXT命令が実行された時に呼ばれる命令です。ホストプログラムに文字列を表示するコードを記述します。ベースの関数はありません。
- TABLE命令 - スクリプトでTABLE命令が実行された時に呼ばれます。フラグ用のテーブルの設定を禁止するなどの処理を記述します。ベースの関数はSETTABLEです。
- CALL命令 - スクリプトで命令が実行された時に呼ばれます。インタプリタとは別のテーブルに設定されている関数のスクリプトからの呼び出し等を記述できます。ベースの関数はCALLFANCです。
- WARNING命令 - スクリプトやインタプリタでWARNING命令が実行された時に呼ばれます。ベースの関数を呼ばないことで特定の警告メッセージの握りつぶし等を記述できます。ベースの関数はADDMESSAGEです
- ABORT命令 - スクリプトやインタプリタでABORT命令が実行された時に呼ばれます。ベースの関数を呼ばないことで特定のエラーメッセージの握りつぶし等を記述できます。ベースの関数はABORTSCRIPTです
コード
インタプリタ実行コード例
require('SSI')
--インタプリタオブジェクト作成関数
local Create = function()
obj = SSI.Create()
--ホストプログラムやライブラリに合わせて必要な関数を実装・上書きする
obj.TEXT = function(self, script, text)
print(text)
end
return obj
end
--インタプリタオブジェクトの作成
local ssi = Create()
--スクリプトの実行。
--successがtrueなら正常終了、falseなら異常終了。 errにエラー・警告メッセージが格納される。
local success, err = ssi:Start('output.txt', nil, "END")
print(err)
インタプリタ本体
SSI = {
Create = function()
local obj = {}
--内部変数及び内部関数
obj._Internal = {
pin = -1,
abort = true,
result = false,
running = false,
valtable = {},
startlabel = "",
errmsg = "",
--内部変数の初期化
Init = function(self)
self._Internal.pin = -1
self._Internal.abort = false
self._Internal.result = false
self._Internal.running = true
self._Internal.valtable = {}
self._Internal.startlabel = ""
self._Internal.errmsg = ""
end,
--スクリプトの実行開始
Process = function(self, script)
if type(script) ~= "table" or #script == 0 then
self.ABORT(self, script, "INVALIDSCRIPT", "Invalid Script Error: Block Data is Not Table or Blank Table.")
return false
end
if self._Internal.abort then return self._Internal.result end
local pin
if self._Internal.startlabel ~= "" then
self.GOTO(self, script, self._Internal.startlabel)
self._Internal.startlabel = ""
end
if self._Internal.pin > 0 then
pin = self._Internal.pin
self._Internal.pin = -1
else
pin = 1
end
local len = #script
--スクリプトの実行
while pin <= len do
local data = script[pin]
local dtype = type(data)
if dtype == "string" then
--テキストの処理
self.TEXT(self, script, data)
elseif dtype == "table" then
--命令・ブロックの処理
if #data == 0 then return end
if type(data[1]) == "string" then
--文字列なら命令を呼び出し
self.CALL(self, nil, data)
elseif type(data[1]) == "table" then
--table型の変数ならブロックの処理開始
self.BLOCK(self, script, data[1])
--elseif type(data[1]) == "function" then
--function型の値ならそのまま呼び出し
--self.INVOKE(self, script, data[1], data)
else
self.ABORT(self, script, "INVALIDSCRIPT", "Invalid Script Error: Statement Data Type "..type(data[1]).." is Not Suppot.")
end
else
self.ABORT(self, script, "INVALIDSCRIPT", "Invalid Script Error: Statement Data Type "..dtype.." is Not Suppot.")
end
--abortがtrueの場合は強制終了
if self._Internal.abort then return self._Internal.result end
if self._Internal.pin > 0 then
pin = self._Internal.pin
self._Internal.pin = -1
end
pin = pin + 1
end
return true
end,
}
--スクリプトの実行開始
--[[
ssi:Start(スクリプト, フラグ用テーブル, 実行開始位置)
]]
obj.Start = function(self, script, valtable, jump)
if type(self.Start) ~= "function" or self._Internal.running then
return false, "Worng Function Call."
end
self.INIT(self, nil)
self.TABLE(self, nil, valtable)
local result
if type(script) == "table" then
result = self.BLOCK(self, {}, script, jump)
elseif type(script) == "string" then
result = self.LOAD(self, {}, script, jump)
else
return false, "Worng Function Call."
end
self._Internal.running = false
self._Internal.abort = true
return result, self._Internal.errmsg
end
--INIT拡張命令
--スクリプト初期化
obj.INIT = function(self, script)
if self.INITSCRIPT(self, script) then
return
end
end
--TEXT拡張命令
--テキストの表示
obj.TEXT = function(self, script, text)
--デバッグ用
print(text)
end
--TABLE拡張命令
--フラグ保管テーブルの設定
obj.TABLE = function(self, script, valtable)
self.SETTABLE(self, script, valtable)
end
--CALL拡張命令
--命令(関数)の呼び出し
obj.CALL = function(self, script, data)
self.CALLFUNC(self, script, data, nil)
end
--LOAD拡張命令
--スクリプトファイルの読み込み・実行
obj.LOAD = function(self, script, fname, jump)
return self.LOADFILE(self, script, fname, jump)
end
--WARNING拡張命令
--警告メッセージの設定
obj.WARNING = function(self, script, type, msg)
self.ADDMESSAGE(self, script, msg)
end
--ABORT拡張命令
--異常終了の設定
obj.ABORT = function(self, script, type, msg)
self.ABORTSCRIPT(self, script, msg)
end
--内部命令
--スクリプト実行のための初期化
obj.INITSCRIPT = function(self, script)
if self._Internal.running then
self.WARNING(self, script, "INVALIDCALL", ": Script Already Running.")
return false
end
if type(script) ~= "nil" then
self.WARNING(self, script, "INVALIDCALL", ": Calling INIT Statements from Scripts is Prohibited.")
return false
end
self._Internal.Init(self)
return true
end
--内部命令
--フラグ設定用のテーブルを設定
obj.SETTABLE = function(self, script, valtable)
if type(valtable) == "table" then
self._Internal.valtable = valtable
end
end
--内部命令
--関数を実行
obj.INVOKE = function(self, script, func, data)
local success
local err
--スクリプトからFUNC関係命令の呼び出しは禁止
if type(script) ~= "nil" then
self.WARNING(self, script, "INVALIDCALL", ": Calling Function Statements from Scripts is Prohibited.")
return
end
if type(func) ~= "function" then
self.WARNING(self, script, "INVALIDARG", "Invalid Argment: INVOKE "..tostring(func))
return
end
success, err = pcall(func, self, script, data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11])
--実行に失敗したら異常終了
if not success then
self.ABORT(self, script, "INVOKEFAIL", "Invoke Failed: "..err)
end
end
--内部命令
--CALL拡張命令で渡された関数または文字列から取得した関数をINVOKE命令に渡す
obj.CALLFUNC = function(self, script, data, func)
if type(data[1]) == "string" and (data[1] == "CALL" or data[1] == "CALLFUNC") then
--table.remove(data, 1)
--self.CALL(self, script, data)
self.WARNING(self, script, "INVALIDCALL", ": Calling Function Statements from Scripts is Prohibited.")
return
end
if type(func) ~= "function" then
if type(self[data[1]]) == "function" then
func = self[data[1]]
else
self.ABORT(self, script, "INVOKEFAIL", "Invoke Failed: Function Not Found.")
end
end
self.INVOKE(self, script, func, data)
end
--BLOCK命令
--ブロックを実行
obj.BLOCK = function(self, script, block, jump)
if type(jump) == "string" then
self._Internal.startlabel = jump
end
return self._Internal.Process(self, block)
end
--LOADFILE命令
--スクリプトファイルを読み込んでブロックとして実行
obj.LOADFILE = function(self, script, fname, jump)
local chunk
local err
if type(fname) == "string" then
chunk, err = loadfile(fname)
elseif type(fname) == "function" then
chunk = fname
else
self.ABORT(self, nil, "LOADERROR", "LoadScript Error: Invalid Argument.")
return false
end
if chunk == nil then
self.ABORT(self, nil, "LOADERROR", "LoadScript Error:"..err)
return false
end
return self.LOADCHUNK(self, script, chunk, jump)
end
--LOADSTRING命令
--スクリプト文字列を読み込んでブロックとして実行
obj.LOADSTRING = function(self, script, str, jump)
if type(str) ~= "string" then
self.ABORT(self, nil, "LOADERROR", "LoadString Error: Invalid Argument.")
return false
end
local chunk, err = load(str)
if chunk == nil then
self.ABORT(self, nil, "LOADERROR", "LoadString Error:"..err)
return false
end
return self.LOADCHUNK(self, script, chunk, jump)
end
--LOADCHUNK
--loadやloadfile関数の戻り値であるchunkを読み込んでブロックとして実行
obj.LOADCHUNK = function(self, script, chunk, jump)
if type(chunk) ~= "function" then
self.ABORT(self, nil, "LOADERROR", "LoadChunk Error: Invalid Argument.")
return false
end
--スクリプトをLuaで実行してテーブルとして読み込み
local block = nil
local success, err = pcall(function() block = chunk() end)
if not success or type(block) ~= "table" then
self.ABORT(self, nil, "LOADERROR", "LoadChunk Error:"..err)
return false
end
return self.BLOCK(self, script, block, jump)
end
--LABEL命令
--GOTO命令の飛び先をセット
obj.LABEL = function(self, script, label)
end
--GOTO命令
--LABEL命令で定義した飛び先にジャンプ
obj.GOTO = function(self, script, label)
if type(label) ~= "string" then
self.WARNING(self, script, "INVALIDARG", "Invalid Argment: GOTO "..tostring(label))
return
end
for i = 1,#table do
if type(script[i]) == "table" and type(script[i][1]) == "string" and type(script[i][2]) == "string" then
if script[i][1] == "LABEL" and script[i][2] == label then
self._Internal.pin = i
return
end
end
end
end
-- SET命令
-- フラグにtrueまたは指定した値やLuaコードの戻り値をセット
obj.SET = function(self, script, name, data)
if type(name) == "string" then
if type(data) == "function" then
self._Internal.valtable[name] = data()
elseif type(data) ~= "nil" then
self._Internal.valtable[name] = data
else
self._Internal.valtable[name] = true
end
else
self.WARNING(self, script, "INVALIDARG", "Invalid Argment: SET "..tostring(name))
end
end
-- UNSET命令
-- フラグにfalseをセット
obj.UNSET = function(self, script, name)
if type(name) == "string" then
self._Internal.valtable[name] = false
else
self.WARNING(self, script, "INVALIDARG", "Invalid Argment: UNSET "..tostring(name))
end
end
-- IF命令
-- 指定したフラグがtrueなら3つめ、falseなら4つめに指定したラベルへジャンプ
-- (4つめは省略可、その場合は次の要素へ)
obj.IF = function(self, script, name, truejmp, falsejmp)
if type(name) ~= "string" then
self.WARNING(self, script, "INVALIDARG", "Invalid Argment: IF "..tostring(name))
return
end
if self._Internal.valtable[name] then
self.GOTO(self, script, truejmp)
else
self.GOTO(self, script, falsejmp)
end
end
--BREAK命令
obj.BREAK = function(self, script)
self._Internal.pin = #script
end
--END命令
obj.END = function(self, script)
self._Internal.abort = true
self._Internal.result = true
end
obj.ADDMESSAGE = function(self, script, msg)
if type(msg) ~= "string" then
self._Internal.errmsg = self._Internal.errmsg..msg.."\n"
else
self.WARNING(self, script, "INVALIDARG", "Invalid Argment: ADDMESSAGE "..tostring(msg))
end
end
--ABORTSCRIPT命令
obj.ABORTSCRIPT = function(self, script, msg)
if type(msg) ~= "string" then
msg = "Script aborted."
end
self.ADDMESSAGE(self, script, msg)
self._Internal.abort = true
end
return obj
end
}
return SSI