Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# https://EditorConfig.org

root = true

[*]
charset = utf-8
end_of_line = unset
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
max_line_width = 64
9 changes: 9 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ default:

unit_test:
@nodemon --exec 'echo -en "\n\n\n\n------------- BUSTED -------------\n"; busted spec' -e 'lua'

test TAG:
@busted spec --tags {{TAG}}

tests:
@busted spec

lint FILE='':
@stylua --column-width 65 {{FILE}}
47 changes: 46 additions & 1 deletion metalua/compiler/ast_to_src.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ function M.new(seen_comments, w)
-- Number of linebreaks
_lines = 0,
-- wrap length
wrap = w or 80
wrap = w or 80,
-- Last source line number emitted (used to detect blank-line gaps)
_last_nonempty_src_line = 0
}
return setmetatable(self, M)
end
Expand Down Expand Up @@ -190,6 +192,32 @@ function M:nltempindent(extra)
self.current_indent = depth
end

----------------------------------------------------------------
--- Reset last non-empty source line to the current position
--- @param pos position|{line: integer}|{l: integer}
----------------------------------------------------------------
function M:emptyline_gap_reset(pos)
local line = pos and (pos.line or pos.l)
-- never go backwards
if line and line > self._last_nonempty_src_line then
self._last_nonempty_src_line = line
end
end

----------------------------------------------------------------
--- Detect the length of emptyline sequence in the source,
--- that precedes the current position
--- @param pos position|{line: integer}|{l: integer}
--- @return integer
----------------------------------------------------------------
function M:emptyline_gap_before(pos)
local line = pos and (pos.line or pos.l)
if line and line > self._last_nonempty_src_line then
return line - self._last_nonempty_src_line
end
return 0
end

----------------------------------------------------------------
--- Keywords, which are illegal as identifiers.
----------------------------------------------------------------
Expand Down Expand Up @@ -429,6 +457,13 @@ function M:node(node)
if co.position == pos then
--- comes _after_ a previous expression
if co.position == 'last' then self:nl() end
--- if there was a 'gap' preceding the comment,
--- we preserve it by emitting exactly one empty line
if co.position == 'first' then
if self:emptyline_gap_before(co.first) >= 2 then
self:nl()
end
end
--- preserve existing newlines
local lines = string.lines(co.text)
if co.multiline then
Expand Down Expand Up @@ -493,11 +528,18 @@ function M:node(node)
end
--- comes _before_ the next expression
if co.position == 'first' then self:nl() end
--- advance non-empty source line tracker to this comment's last line
self:emptyline_gap_reset(co.last)
end
end
end

show_comments('first')
-- advance non-empty source line tracker,
-- used for detecting emptyline gaps in the source
if node.lineinfo and node.lineinfo.first then
self:emptyline_gap_reset(node.lineinfo.first)
end
if not node.tag then --- tagless block.
self:list(node, self.nl)
else
Expand All @@ -515,6 +557,9 @@ function M:node(node)
self:acc(" }")
end
end
if node.lineinfo and node.lineinfo.last then
self:emptyline_gap_reset(node.lineinfo.last)
end
show_comments('last')
end

Expand Down
175 changes: 175 additions & 0 deletions spec/ast_inputs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,179 @@ local comments = {
' ',
'end',
}),

prep({
'',
'-- emptyline before comment must be preserved',
'function foo(bar)',
' return ("no comment duplication should happen")',
'end'
}),

prep({
'',
'',
'-- multiple emptylines before comment must be squashed',
}, {
'',
'-- multiple emptylines before comment must be squashed',
})
}

local emptylines = {

-- standalone comment

prep({'--- standalone comment does not enforce emptylines'}),
prep({'',
'--- standalone comment preserves emptyline before it'
}),
prep({
'',
'',
'',
'--- standalone comment keeps just one emptyline before it'
},{
'',
'--- standalone comment keeps just one emptyline before it'
}),

-- standalone expression

prep({
'print("standalone expression does not enforce emptylines")'
}),
prep({
'',
'',
'',
'print("standalone expression eliminates emptylines before it")'
}, {
'print("standalone expression eliminates emptylines before it")'
}),

--- expression+comment

prep({
'-- comment before expression does not enforce emptylines',
'print("expectation: no emptylines are injected")',
}),
prep({
'',
'',
'',
'-- comment before expression allows one emptyline before it',
'print("expectation: exactly one emptyline is preserved")',
}, {
'',
'-- comment before expression allows one emptyline before it',
'print("expectation: exactly one emptyline is preserved")',
}),

-- expressions (multiple)

prep({
'print("rule: no gap between adjacent statemets is enforced")',
'print("expecting: no emptyline should be injected")'
}),
prep({
'print("rule: gap between adjacent statements is eliminated")',
'',
'',
'',
'print("expecting: no emptylines preserved")'
}, {
'print("rule: gap between adjacent statements is eliminated")',
'print("expecting: no emptylines preserved")'
}),

-- expressions (multiple) + comment

prep({
'print("rule: comment between statements does not preserve gap")',
'',
'-- comment between statements does not preserve gap',
'print("expecting: emptyline is not preserved")'
}, {
'print("rule: comment between statements does not preserve gap")',
'-- comment between statements does not preserve gap',
'print("expecting: emptyline is not preserved")'
}),

-- standalone block

prep({
'function rule()',
' return ("standalone block does not emit emptylines")',
'end'
}),
prep({
'',
'',
'',
'function rule()',
' return ("standalone block erases emptylines before it")',
'end'
}, {
'function rule()',
' return ("standalone block erases emptylines before it")',
'end'
}),

-- standalone block + comment

prep({
'-- comment before block',
'function rule()',
' return ("comment before block does not emit emptylines")',
'end'
}),
prep({
'',
'',
'',
'-- comment before block',
'function rule()',
' return ("comment before block keeps 1 emptyline before it")',
'end'
}, {
'',
'-- comment before block',
'function rule()',
' return ("comment before block keeps 1 emptyline before it")',
'end'
}),

-- multiple blocks + comment in between

prep({
'function rule()',
' return ("no gaps between blocks are enforced")',
'end',
'-- comment in between',
'function expectation()',
' return ("no emptylines are injected")',
'end',
}),
prep({
'function rule()',
' return ("no gaps between blocks are preserved")',
'end',
'',
'-- comment in between',
'',
'function expectation()',
' return ("no emptylines are kept")',
'end',
}, {
'function rule()',
' return ("no gaps between blocks are preserved")',
'end',
'-- comment in between',
'function expectation()',
' return ("no emptylines are kept")',
'end',
})
}

local wrapping = {
Expand Down Expand Up @@ -1199,12 +1372,14 @@ local todo = {
prep('t = { c = 2 }'),
prep('a = 1'),
}

return {
{ 'basics', basics },
{ 'operators', operators },
{ 'functions', functions },
{ 'self', self },
{ 'comments', comments },
{ 'emptylines', emptylines },
{ 'wrap', wrapping },
{ 'canon', canon },
{ 'full', full },
Expand Down
17 changes: 10 additions & 7 deletions spec/ast_to_src_spec.lua
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
local fmt = string.format

local describe = require("busted").describe
local assert = require("busted").assert
local it = require("busted").it

require("stringutils.string")

local inputs = require("spec.ast_inputs")
local mlc = require('metalua.compiler').new()
local mlc = require("metalua.compiler").new()

local w = 64

Expand All @@ -16,7 +18,7 @@ local parse_prot = function(code)
return pcall(mlc.src_to_ast, mlc, c)
end

describe('ast_to_src #src', function()
describe("ast_to_src #src", function()
local function do_code(ast, seen_comments)
local a2s = mlc:a2s(seen_comments, w)
local code, comments = a2s:run(ast)
Expand All @@ -33,14 +35,15 @@ describe('ast_to_src #src', function()
return code, seen_comments
end

describe('produces ASTs', function()
describe("produces ASTs", function()
for _, test_t in pairs(inputs) do
local tag = test_t[1]
local tests = test_t[2]
describe('for ' .. tag, function()
describe("for #" .. tag, function()
for i, tc in ipairs(tests) do
local input = tc[1]
local output = tc[2]
local testtag = fmt("%s_%s", tag, i)

local ok, r = parse_prot(input)
local result = {}
Expand All @@ -61,15 +64,15 @@ describe('ast_to_src #src', function()
end

--- remove trailing newline
if result[#result] == '' then
if result[#result] == "" then
table.remove(result)
end
it('matches ' .. i, function()
it("matching rule #" .. testtag, function()
assert.same(output, result)
assert.is_true(parse_prot(output))
end)
else
print('syntax error in input #' .. i)
print("syntax error in #" .. testtag)
print(r)
end
end
Expand Down
Loading