nomsu/code_obj.moon
Bruce Hill b6d3cbd61c Misc changes, including text indented interpolations are now indented
relative to the text, not the opening '("', code objects can now remove
all free vars, the REPL uses global vars. Error API is changing a bit.
2019-01-01 15:07:10 -08:00

256 lines
7.8 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
__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: => #@text!
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'
unless type(b) == 'string'
b = b\text!
line = match(b, "\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
b = b\text!
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}