TIC-80で何かのREPLを作ろう

TIC-80でプログラミング言語のREPLを作ろー

ちゃんとしたエディタが使いたいので、とりあえずLinuxに入ってるLuaで動くやつを作ろー

なんの言語がいーい？

```
829： リスナーさん ：2018/12/11(火) 22:36:43
    うーん、Cｗ
```

はいっ、というわけでね！　シーワラ作っていきましょう。こんな感じの対話がしたいぞ。

```
> 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
```