Download
187 lines 6.1 KiBMoonScriptModified 2019-03-15 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
-- This file contains the datastructures used to represent parsed Nomsu syntax trees,
-- as well as the logic for converting them to Lua code.
{:insert, :remove, :concat} = table
{:Source} = require "code_obj"
{:List, :Dict} = require 'containers'
Files = require 'files'
unpack or= table.unpack

as_lua = =>
    if type(@) == 'number'
        return tostring(@)
    if mt = getmetatable(@)
        if _as_lua = mt.as_lua
            return _as_lua(@)
    return @as_lua! if @as_lua
    error("Not supported: #{@}")

local SyntaxTree
class SyntaxTree
    __tostring: =>
        bits = [type(b) == 'string' and b\as_lua! or tostring(b) for b in *@]
        for k,v in pairs(@)
            unless bits[k] or k == 'type' or k == 'source'
                table.insert(bits, "#{k}=#{type(v) == 'string' and v\as_lua! or v}")
        return "#{@type}{#{table.concat(bits, ", ")}}"

    __eq: (other)=>
        return false if type(@) != type(other) or #@ != #other or getmetatable(@) != getmetatable(other)
        return false if @type != other.type
        for i=1,#@
            return false if @[i] != other[i]
        return true

    as_lua: =>
        bits = [as_lua(b) for b in *@]
        for k,v in pairs(@)
            unless bits[k]
                table.insert(bits, "[ #{as_lua(k)}]=#{as_lua(v)}")
        return "SyntaxTree{#{table.concat(bits, ", ")}}"

    @source_code_for_tree: setmetatable({}, {
        __index:(t)=>
            s = t.source
            f = Files.read(s.filename)
            return f
        __mode: "k"
    })
    get_source_file: => @@source_code_for_tree[@]
    get_source_code: => @@source_code_for_tree[@]\sub(@source.start, @source.stop-1)

    add: (...)=>
        n = #@
        for i=1,select('#', ...)
            @[n+i] = select(i, ...)
        @stub = nil

    with: (fn)=>
        if type(fn) == 'table'
            replacements = fn
            fn = (t)->
                if t.type == "Var"
                    if r = replacements[t\as_var!]
                        return r

        replacement = fn(@)
        if replacement == false then return nil
        if replacement
            -- Clone the replacement, so we can give it a proper source/comments
            if SyntaxTree\is_instance(replacement)
                replacement = {k,v for k,v in pairs replacement}
                replacement.source = @source
                replacement.comments = {unpack(@comments)} if @comments
                replacement = SyntaxTree(replacement)
        else
            replacement = {source:@source, comments:@comments and {unpack(@comments)}}
            changes = false
            for k,v in pairs(@)
                replacement[k] = v
                if SyntaxTree\is_instance(v)
                    r = v\with(fn)
                    continue if r == v or r == nil
                    changes = true
                    replacement[k] = r
            return @ unless changes
            replacement = SyntaxTree(replacement)
        return replacement

    contains: (subtree)=>
        if subtree == @ then return true
        for k,v in pairs(@)
            if SyntaxTree\is_instance(v)
                return true if v\contains(subtree)
        return false

    get_args: =>
        assert(@type == "Action" or @type == "MethodCall", "Only actions and method calls have arguments")
        args = {}
        if @type == "MethodCall"
            args[1] = @[1]
            for i=2,#@
                for tok in *@[i]
                    if type(tok) != 'string' then args[#args+1] = tok
        else
            for tok in *@
                if type(tok) != 'string' then args[#args+1] = tok
        return args

    get_stub: =>
        switch @type
            when "Action"
                stub_bits = {}
                arg_i = 1
                for a in *@
                    if type(a) == 'string'
                        stub_bits[#stub_bits+1] = a
                    else
                        stub_bits[#stub_bits+1] = arg_i
                        arg_i += 1
                while type(stub_bits[#stub_bits]) == 'number'
                    stub_bits[#stub_bits] = nil
                return concat stub_bits, " "
            when "MethodCall"
                return "0, "..table.concat([@[i]\get_stub! for i=2,#@], "; ")
            else
                error("#{@type}s do not have stubs")

    as_var: =>
        assert(@type == "Var")
        if type(@[1]) == 'string'
            return @[1]
        else
            return @[1]\get_stub!

    matching: (patt)=>
        if patt.type == "Var"
            return {[patt\as_var!]:@}
        return nil if patt.type != @type
        return nil if patt.type == "Action" and patt\get_stub! != @get_stub!
        -- TODO: support vararg matches like (\(say 1 2 3), matching \(say *$values))
        return nil if #@ != #patt
        match = {}
        for i=1,#@
            v = @[i]
            pv = patt[i]
            return nil if type(v) != type(pv)
            if type(v) != 'table'
                return nil unless v == pv
            else
                m = v\matching(pv)
                return nil unless m
                for mk,mv in pairs(m)
                    return nil if match[mk] and match[mk] != mv
                    match[mk] = mv
        return Dict(match)

    _breadth_first: =>
        coroutine.yield @
        for child in *@
            if getmetatable(child) == SyntaxTree.__base
                child\_breadth_first!
        return
    breadth_first: => coroutine.create(-> @_breadth_first!)

    _depth_first: =>
        coroutine.yield @
        for child in *@
            if getmetatable(child) == SyntaxTree.__base
                child\_depth_first!
        return
    depth_first: => coroutine.create(-> @_depth_first!)

    @is_instance: (t)=>
        type(t) == 'table' and getmetatable(t) == @__base

SyntaxTree.__base.__type = "a Syntax Tree"

getmetatable(SyntaxTree).__call = (t, ...)=>
    if type(t.source) == 'string'
        t.source = Source\from_string(t.source)
    setmetatable(t, @__base)
    for i=1,select("#", ...)
        t[i] = select(i, ...)
    if t.type == 'Action'
        t.stub = t\get_stub!
    return t

return SyntaxTree