aboutsummaryrefslogtreecommitdiff

– This file contains objects that are used to track code positions and incrementally – build up generated code, while keeping track of where it came from, and managing – indentation levels. {:insert, :remove, :concat} = table unpack or= table.unpack local LuaCode, NomsuCode, Source

class Source new: (@filename, @start, @stop)=>

@from_string: (str)=>
    filename,start,stop = str\match("^@(.-)%[(%d+):(%d+)%]$")
    unless filename
        filename,start = str\match("^@(.-)%[(%d+)%]$")
    return @(filename or str, tonumber(start or 1), tonumber(stop))

@is_instance: (x)=> type(x) == 'table' and x.__class == @

__tostring: => "@#{@filename}[#{@start}#{@stop and ':'..@stop or ''}]"

as_lua: => "Source(#{@filename\as_lua!}, #{@start}#{@stop and ', '..@stop or ''})"

__eq: (other)=>
    getmetatable(@) == getmetatable(other) and @filename == other.filename and @start == other.start and @stop == other.stop

__lt: (other)=>
    assert(@filename == other.filename, "Cannot compare sources from different files")
    return if @start == other.start
        (@stop or @start) < (other.stop or other.start)
    else @start < other.start

__le: (other)=>
    assert(@filename == other.filename, "Cannot compare sources from different files")
    return if @start == other.start
        (@stop or @start) <= (other.stop or other.start)
    else @start <= other.start

__add: (offset)=>
    if type(self) == 'number'
        offset, self = self, offset
    else if type(offset) != 'number' then error("Cannot add Source and #{type(offset)}")
    return Source(@filename, @start+offset, @stop)

class Code new: (…)=> @bits = {} @add(…)

@from: (source, ...)=>
    inst = self(...)
    if type(source) == 'string'
        source = Source\from_string(source)
    inst.source = source
    return inst

@is_instance: (x)=> type(x) == 'table' and x.__class == @

text: =>
    if @__str == nil
        buff, indent = {}, 0
        {:match, :gsub, :rep} = string
        for i,b in ipairs @bits
            if type(b) == 'string'
                if spaces = match(b, "\n([ ]*)[^\n]*$")
                    indent = #spaces
            else
                b = b\text!
                if indent > 0
                    b = gsub(b, "\n", "\n"..rep(" ", indent))
            buff[#buff+1] = b
        @__str = concat(buff, "")
    return @__str

last: (n)=>
    if @__str
        return @__str\sub(-n, -1)
    last = ""
    for i=#@bits,1,-1
        b = @bits[i]
        last = (type(b) == 'string' and b\sub(-(n-#last)) or b\last(n-#last))..last
        break if #last == n
    return last

first: (n)=>
    if @__str
        return @__str\sub(1,n)
    first = ""
    for b in *@bits
        first ..= type(b) == 'string' and b\sub(1,n-#first+1) or b\first(n-#first+1)
        break if #first == n
    return first

__tostring: => @text!

as_lua: =>
    if @source
        "#{@__class.__name}:from(#{concat {tostring(@source)\as_lua!, unpack([b\as_lua! for b in *@bits])}, ", "})"
    else
        "#{@__class.__name}(#{concat [b\as_lua! for b in *@bits], ", "})"

__len: =>
    if @__str
        return #@__str
    len = 0
    for b in *@bits
        len += #b
    return len

match: (...)=> @text!\match(...)

gmatch: (...)=> @text!\gmatch(...)

dirty: =>
    @__str = nil
    @_trailing_line_len = nil
    -- Multi-line only goes from false->true, since there is no API for removing bits
    @_is_multiline = nil if @_is_multiline == false

add: (...)=>
    n = select("#",...)
    match = string.match
    bits = @bits
    for i=1,n
        b = select(i, ...)
        assert(b, "code bit is nil")
        assert(not Source\is_instance(b), "code bit is a Source")
        if b == '' then continue
        bits[#bits+1] = b
    @dirty!

trailing_line_len: =>
    if @_trailing_line_len == nil
        @_trailing_line_len = #@text!\match("[^\n]*$")
    return @_trailing_line_len

is_multiline: =>
    if @_is_multiline == nil
        match = string.match
        @_is_multiline = false
        for b in *@bits
            if type(b) == 'string'
                if match(b, '\n')
                    @_is_multiline = true
                    break
            elseif b\is_multiline!
                @_is_multiline = true
                break
    return @_is_multiline

concat_add: (values, joiner, wrapping_joiner)=>
    wrapping_joiner or= joiner
    match = string.match
    bits = @bits
    line_len = 0
    for i=1,#values
        b = values[i]
        if i > 1
            if line_len > 80
                bits[#bits+1] = wrapping_joiner
                line_len = 0
            else
                bits[#bits+1] = joiner
        bits[#bits+1] = b
        b.dirty = error if type(b) != 'string'
        line = b\match("\n([^\n]*)$")
        if line
            line_len = #line
        else
            line_len += #b
    @dirty!

prepend: (...)=>
    n = select("#",...)
    bits = @bits
    for i=#bits+n,n+1,-1
        bits[i] = bits[i-n]
    for i=1,n
        b = select(i, ...)
        b.dirty = error if type(b) != 'string'
        bits[i] = b
    @dirty!

parenthesize: =>
    @prepend "("
    @add ")"

class LuaCode extends Code tostring: Code.tostring aslua: Code.aslua len: Code.len new: (…)=> super … @free_vars = {}

add_free_vars: (vars)=>
    return unless #vars > 0
    seen = {[v]:true for v in *@free_vars}
    for var in *vars
        assert type(var) == 'string'
        unless seen[var]
            @free_vars[#@free_vars+1] = var
            seen[var] = true
    @dirty!

remove_free_vars: (vars=nil)=>
    vars or= @get_free_vars!
    return unless #vars > 0
    removals = {}
    for var in *vars
        assert type(var) == 'string'
        removals[var] = true

    stack = {self}
    while #stack > 0
        lua, stack[#stack] = stack[#stack], nil
        for i=#lua.free_vars,1,-1
            free_var = lua.free_vars[i]
            if removals[free_var]
                remove lua.free_vars, i
        for b in *lua.bits
            if type(b) != 'string'
                stack[#stack+1] = b
    @dirty!

get_free_vars: =>
    vars, seen = {}, {}
    gather_from = =>
        for var in *@free_vars
            unless seen[var]
                seen[var] = true
                vars[#vars+1] = var
        for bit in *@bits
            unless type(bit) == 'string'
                gather_from bit
    gather_from self
    return vars

declare_locals: (to_declare=nil)=>
    to_declare or= @get_free_vars!
    if #to_declare > 0
        @remove_free_vars to_declare
        @prepend "local #{concat to_declare, ", "};\n"
    return to_declare

make_offset_table: =>
    assert @source, "This code doesn't have a source"
    -- Return a mapping from output (lua) character number to input (nomsu) character number
    lua_to_nomsu, nomsu_to_lua = {}, {}
    walk = (lua, pos)->
        for b in *lua.bits
            if type(b) == 'string'
                if lua.source
                    lua_to_nomsu[pos] = lua.source.start
                    nomsu_to_lua[lua.source.start] = pos
            else
                walk b, pos
            pos += #b
    walk self, 1
    return {
        nomsu_filename:@source.filename
        lua_filename:tostring(@source)..".lua", lua_file:@text!
        :lua_to_nomsu, :nomsu_to_lua
    }

parenthesize: =>
    @prepend "("
    @add ")"

class NomsuCode extends Code tostring: Code.tostring aslua: Code.aslua len: Code.len

Code.base.add1joined_with = assert Code.base.concatadd Code.base.add = assert Code._base.add

return {:Code, :NomsuCode, :LuaCode, :Source}