nomsu/code_obj.moon

278 lines
8.3 KiB
Plaintext

-- 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
as_lua: Code.as_lua
__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
as_lua: Code.as_lua
__len: Code.__len
Code.__base.add_1_joined_with = assert Code.__base.concat_add
Code.__base.add = assert Code.__base.add
return {:Code, :NomsuCode, :LuaCode, :Source}