TIC-80でプログラミング言語のREPLを作ろー
ちゃんとしたエディタが使いたいので、とりあえずLinuxに入ってるLuaで動くやつを作ろー
なんの言語がいーい?
829: リスナーさん :2018/12/11(火) 22:36:43
うーん、Cw
はいっ、というわけでね! シーワラ作っていきましょう。こんな感じの対話がしたいぞ。
> func main() {
print("Hello World");
return 0;
}
#<FUNC>
> main()
Hello World
0
>
電卓プログラムをベースにしよう。文法はこれ!
program := stmt*
stmt := exp-stmt |
compound-stmt |
if-stmt |
func-stmt
func-stmt := "func" identifier "(" identifer-list ")" compound-stmt
exp-stmt := expression ";"
compound-stmt := "{" stmt* "}"
if-stmt := "if" "(" expression ")" stmt [elsif "(" expression ")" stmt]* [else stmt]?
expression := arith-exp |
func-call |
assign
func-call := identifer "(" expression ["," expression]* ")"
assign := identifer "=" expression
arith-exp := term [ ['+'|'-'] term ]*
term := factor [ ['*'|'/'] factor]*
factor := [ NUMBER | STRING | IDENTIFIER | '(' arith_exp ')' | '-' factor ]
四則演算とLua関数呼び出しと変数代入ができるところまでいった。
> x = 3;
nil
> print("Hello World", 100 * x);
Hello World 300
nil
>
セミコロンに厳しい。C#とか、ステートメントベースの言語のREPLは、文として読み込んでみて失敗したら式として読み直しているのかな?
> x = 3
lua: /home/plonk/g/tic-80/cw.lua:219: unexpected symbol: "END"; was expecting ";"
stack traceback:
[C]: in function 'error'
/home/plonk/g/tic-80/cw.lua:219: in function 'expect'
/home/plonk/g/tic-80/cw.lua:107: in function 'exp_stmt'
/home/plonk/g/tic-80/cw.lua:100: in function </home/plonk/g/tic-80/cw.lua:94>
(...tail calls...)
/home/plonk/g/tic-80/cw.lua:320: in main chunk
[C]: in ?
構文木は Lua データ構造で、Lua文字列をCwの識別子として使っている。これとCwの文字列リテラルは区別しなきゃいけないので、文字列リテラルは1つ目の要素が "string" なテーブルにした(例: "hoge" → {"string","hoge"})。だけど、これだと string という名前の関数の呼び出しと区別が付かなかったり、Lua関数に渡す時に変換しなきゃいけないからよろしくなかった。
以下ソースコード。
-- うーん、Cw
--
-- 使用例:
-- $ lua cw.lua
--
-- 文法:
-- program := stmt*
-- stmt := exp-stmt |
-- compound-stmt |
-- if-stmt |
-- func-stmt
-- func-stmt := "func" identifier "(" identifer-list ")" compound-stmt
-- exp-stmt := expression ";"
-- compound-stmt := "{" stmt* "}"
-- if-stmt := "if" "(" expression ")" stmt [elsif "(" expression ")" stmt]* [else stmt]?
-- expression := arith-exp |
-- func-call |
-- assign
-- func-call := identifer "(" expression ["," expression]* ")"
-- assign := identifer "=" expression
-- arith-exp := term [ ['+'|'-'] term ]*
-- term := factor [ ['*'|'/'] factor]*
-- factor := [ NUMBER | STRING | IDENTIFIER | '(' arith_exp ')' | '-' factor ]
-- 入力行をトークンの列にする。
function tokenize(str)
local tokens = {}
local init = 1
local m
local read = function (pat)
m = string.match(str, pat, init)
if m then
init = init + #m
end
return m
end
while init <= #str do
if read("^[%.%d]+") then
table.insert(tokens, tonumber(m))
elseif read("^%s+") then
elseif read("^[%+%-*/%^()]") then
table.insert(tokens, m)
elseif read("^;") then
table.insert(tokens, m)
elseif read("^\"[^\"]*\"") then
local len = #m
table.insert(tokens, {"string", string.sub(m, 2, len-1)})
elseif read("^[A-Za-z_]+") then
table.insert(tokens, m)
elseif read("^[,=]") then
table.insert(tokens, m)
else
error("syntax error")
end
end
table.insert(tokens, "END")
return tokens
end
-- printデバッグ用、任意の型の値を文字列化。
function inspect(v)
local t = type(v)
if t=="table" then
local buf = "{"
local first = true
for key,val in pairs(v) do
if not first then
buf = buf..","
else
first = false
end
buf = buf..string.format("[%s]=%s",
inspect(key), inspect(val))
end
buf = buf .. "}"
return buf
elseif t=="number" then
return string.format("%g", v)
elseif t=="string" then
return string.format("%q", v)
else
return tostring(v)
end
end
-- トークン列をパースする。
function parse(tokens)
local sym
local nextsym, accept, expect
local program, arith_exp, term, factor, nonexpo
function nextsym()
sym = table.remove(tokens,1)
end
function program()
local r = {"progn"}
while true do
if accept("END") then
break
else
table.insert(r, exp_stmt())
end
end
return r
end
function exp_stmt()
local e = expression()
expect(";")
return e
end
function expression()
if type(sym) == "string" and tokens[1] == "(" then
return func_call()
elseif type(sym) == "string" and tokens[1] == "=" then
return assign()
else
return arith_exp()
end
end
function assign()
local name = identifier()
expect("=")
return {"set", name, expression()}
end
function func_call()
local r = { identifier() }
expect("(")
table.insert(r, expression())
while accept(",") do
table.insert(r, expression())
end
expect(")")
return r
end
function identifier()
if type(sym) == "string" then
local sym1 = sym
nextsym()
return sym1
else
error("identifier")
end
end
function arith_exp()
local r = {"+"}
table.insert(r, term())
while true do
if accept("+") then
table.insert(r, term())
elseif accept("-") then
table.insert(r, {"-@", term()})
else
break
end
end
if #r == 2 then
return r[2]
else
return r
end
end
function accept(s)
if sym == s then
nextsym()
return true
else
return false
end
end
function term()
local r = {"*"}
table.insert(r, factor())
while true do
if accept("*") then
table.insert(r, factor())
elseif accept("/") then
table.insert(r, {"/", 1, factor()})
else
break
end
end
if #r == 2 then
return r[2]
else
return r
end
end
function factor()
if type(sym) == 'number' then
local sym1 = sym
nextsym()
return sym1
elseif accept('(') then
local e = arith_exp()
expect(')')
return e
elseif accept('-') then
return {"-@", factor()}
elseif type(sym) == "string" then
local sym1 = sym
nextsym()
return sym1
elseif type(sym) == 'table' then
local tbl = sym
if tbl[1] == "string" then
nextsym()
return tbl
else
error("factor (table)")
end
else
error("factor")
end
end
function expect(s)
if not accept(s) then
error("unexpected symbol: " .. inspect(sym) .. "; was expecting " .. inspect(s))
end
end
nextsym()
return program()
end
function lua_value(v)
if type(v)=="table" and v[1] == "string" then
return v[2]
else
return v
end
end
function cw_value(v)
if type(v)=="string" then
return {"string", v}
else
return v
end
end
function eval(exp, env)
if type(exp) == "string" then
return env[exp]
elseif type(exp) == "number" then
return exp
elseif type(exp)=="table" then
if exp[1] == "string" then
return exp
elseif exp[1] == "progn" then
local r
for i=2,#exp do
r = eval(exp[i], env)
end
return r
elseif exp[1] == "set" then
env[exp[2]] = eval(exp[3], env)
return nil
else
-- 関数呼び出し
local f = eval(exp[1], env)
local arglist = {}
for i=2,#exp do
table.insert(arglist, lua_value(eval(exp[i], env)))
end
return cw_value(f(unpack(arglist)))
end
elseif exp == nil then
return nil
else
error("invalid exp")
end
end
-- テスト
-- print(inspect(parse(tokenize("1;"))))
-- print(inspect(parse(tokenize("1+1;"))))
-- print(inspect(parse(tokenize("1-1+1;"))))
-- print(inspect(parse(tokenize("1*1;"))))
-- print(inspect(parse(tokenize("\"hello world\";"))))
-- print(inspect(parse(tokenize("f(1);"))))
-- print(inspect(parse(tokenize("print(\"hello world\");"))))
-- exp = parse(tokenize("print(\"hello world\");"))
env = {
print = print,
["+"] = function (...)
local r = 0
local args = {select(1,...)}
for i=1,#args do
r = r + args[i]
end
return r
end,
["-@"] = function (v)
return -v
end,
["*"] = function (...)
local r = 1
local args = {select(1,...)}
for i=1,#args do
r = r * args[i]
end
return r
end,
["/"] = function (a,b)
return a/b
end
}
-- print(eval(exp, env))
-- ドライバーループ。
while true do
io.write("> ")
local line = io.read("*l")
if line then
tokens = tokenize(line)
-- print(inspect(tokens))
exp = parse(tokens)
-- print(inspect(exp))
print(eval(exp, env))
else
break
end
end