1
0

Improved modulo messages

This commit is contained in:
Jon Michael Aanes 2018-03-15 17:47:47 +01:00
parent f8447cdd39
commit 6c7c914da2
2 changed files with 145 additions and 87 deletions

View File

@ -1,5 +1,6 @@
local lexer = assert(require((... and select('1', ...):match('.+%.') or '')..'lua_lang'), '[assert-gooder]: Could not load vital library: lua_lang') local lexer = assert(require((... and select('1', ...):match('.+%.') or '')..'lua_lang'), '[assert-gooder]: Could not load vital library: lua_lang')
local shunting_yard = assert(require((... and select('1', ...):match('.+%.') or '')..'Parser'), '[assert-gooder]: Could not load vital library: Parser')
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -50,24 +51,9 @@ local function get_variable (var_name, info)
return env, 'global' return env, 'global'
end end
--[[
local function get_value_from_lvalue (lvalue, info)
assert(type(lvalue) == 'table')
assert(type(info) == 'table')
-- Base value
ilocal value, var_scope, in_func = get_variable(lvalue[1], info)
-- Sub value
for i = 2, #lvalue do value = value[lvalue[i].value] end
--
return value, var_scope, in_func
end
--]]
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Parsing -- Parsing
local shunting_yard = require 'Parser'
local NO_PARSE_TOKENS = { local NO_PARSE_TOKENS = {
FUNCTION = true, FUNCTION = true,
@ -186,7 +172,6 @@ local function parse (tokens)
-- Postfix the tokens -- Postfix the tokens
local postfix_tokens = shunting_yard(tokens, lua_expression_lang, NUM_ARITY) local postfix_tokens = shunting_yard(tokens, lua_expression_lang, NUM_ARITY)
assert(type(postfix_tokens) == 'table') assert(type(postfix_tokens) == 'table')
print(require'pretty'(postfix_tokens))
-- Create AST from postfix tokens -- Create AST from postfix tokens
local ast_stack = {} local ast_stack = {}
@ -251,40 +236,6 @@ local function parse (tokens)
assert(#ast_stack == 1) assert(#ast_stack == 1)
return ast_stack[1] return ast_stack[1]
--[[
if #tokens == 6 and tokens[1].text == 'type' and tokens[2].token == 'LPAR' and tokens[3].token == 'IDENTIFIER' and tokens[4].token == 'RPAR'and tokens[5].token == 'EQ'and tokens[6].token == 'STRING' then
return {
exp = 'COMPARE',
binop = 'EQ',
[1] = { exp = 'CALL', get_value_token(tokens[1]), get_value_token(tokens[3]) },
[2] = get_value_token(tokens[6]),
}
elseif #tokens == 3 and VALUE_TOKEN[tokens[1].token] and COMPARE_BINOP[tokens[2].token] and VALUE_TOKEN[tokens[3].token] then
return {
exp = 'COMPARE',
binop = tokens[2].token,
[1] = get_value_token(tokens[1]),
[2] = get_value_token(tokens[3])
}
elseif #tokens == 4 and tokens[1].token == 'HASHTAG' and VALUE_TOKEN[tokens[2].token] and COMPARE_BINOP[tokens[3].token] and VALUE_TOKEN[tokens[4].token] then
return {
exp = 'COMPARE',
binop = tokens[3].token,
[1] = { exp = 'UNOP', get_value_token(tokens[2]) } ,
[2] = get_value_token(tokens[4])
}
elseif #tokens == 3 and tokens[1].token == 'IDENTIFIER' and tokens[2].token == 'DOT' and tokens[3].token == 'IDENTIFIER' then
return { exp = 'LVALUE', tokens[1].text, { exp = 'STRING', value = tokens[3].text } }
elseif #tokens == 4 and tokens[1].token == 'IDENTIFIER' and tokens[2].token == 'LBRACK' and VALUE_TOKEN[tokens[3].token] and tokens[4].token == 'RBRACK' then
return { exp = 'LVALUE', tokens[1].text, get_value_token(tokens[3]) }
elseif #tokens == 1 then
return get_value_token(tokens[1])
else
io.stderr:write '[assert-gooder/internal]: Unknown AST structure!\n'
end
--]]
end end
local function for_each_node_in_ast (ast, func) local function for_each_node_in_ast (ast, func)
@ -303,12 +254,17 @@ end
local function populate_ast_with_semantics (ast, info) local function populate_ast_with_semantics (ast, info)
assert(type(ast) == 'table') assert(type(ast) == 'table')
assert(type(info) == 'table') assert(type(info) == 'table')
for_each_node_in_ast(ast, function(node)
if node.token then
assert(not node.ast)
node.exp, node.token = node.token, nil
end
end)
return for_each_node_in_ast(ast, function(node) return for_each_node_in_ast(ast, function(node)
if node.token == 'IDENTIFIER' then if node.exp == 'IDENTIFIER' then
-- TODO: Variable scope, and is it in a function?
node.value, node.scope, node.function_local = get_variable(node.text, info) node.value, node.scope, node.function_local = get_variable(node.text, info)
elseif CONSTANT_VALUE_TOKEN[node.token] then elseif CONSTANT_VALUE_TOKEN[node.exp] then
node.value = CONSTANT_VALUE_TOKEN[node.token](node.text) node.value = CONSTANT_VALUE_TOKEN[node.exp](node.text)
elseif node.exp == 'OP' and node.binop == 'DOT' then elseif node.exp == 'OP' and node.binop == 'DOT' then
assert(node[1].value and node[2].value) assert(node[1].value and node[2].value)
node.value = node[1].value[ node[2].value ] --TODO node.value = node[1].value[ node[2].value ] --TODO
@ -416,7 +372,7 @@ end
local function fmt_lvalue (node, with_scope) local function fmt_lvalue (node, with_scope)
assert(type(node) == 'table') assert(type(node) == 'table')
if node.token == 'IDENTIFIER' then if node.exp == 'IDENTIFIER' then
local base = node.text local base = node.text
if with_scope then base = ('%s \'%s\''):format(node.scope, base) end if with_scope then base = ('%s \'%s\''):format(node.scope, base) end
return base, node.function_local return base, node.function_local
@ -429,19 +385,6 @@ local function fmt_lvalue (node, with_scope)
error 'Not implemented yet!' error 'Not implemented yet!'
end end
--[[
local function ast_to_formal_lvalue (ast)
if ast.token == 'IDENTIFIER' then
return { ast.text, exp = 'LVALUE' }
elseif ast.exp == 'OP' and ast.binop == 'DOT' then
local prev = ast_to_formal_lvalue(ast[1])
prev[#prev+1] = get_value_token(ast[2]).value
return prev
end
assert(false)
end
--]]
local function fmt_prefix (ast, call_info) local function fmt_prefix (ast, call_info)
assert(type(ast) == 'table') assert(type(ast) == 'table')
-- --
@ -476,7 +419,7 @@ local function fmt_table_with_type (val)
-- Conclude: -- Conclude:
if last_key == nil then if last_key == nil then
subtype = (num_visited == 1) and 'empty table' or 'sequence' subtype = (num_visited == 1) and 'empty table' or 'sequence of length '..(num_visited - 1)
end end
end end
@ -503,6 +446,46 @@ local function fmt_val_with_type (val)
return type(val) .. ' ' .. fmt_val(val) return type(val) .. ' ' .. fmt_val(val)
end end
local function is_l_value (ast)
assert(type(ast) == 'table')
if ast.exp == 'OP' and ast.binop == 'DOT' then
return true
elseif ast.exp == 'IDENTIFIER' then
return true
end
return false
end
local function fancy_fmt_seq (seq, ends_with)
ends_with = ends_with or ', and '
assert(type(seq) == 'table')
assert(type(ends_with) == 'string')
local sep = ', '
local l = {}
for i = 1, #seq do
l[#l+1] = fmt_val(seq[i])
l[#l+1] = sep
end
if #seq > 0 then l[#l] = nil end
if #seq > 1 then l[#l-1] = ends_with end
return table.concat(l, '')
end
local function similar_keys_in_table (t, key)
assert(type(t) == 'table')
assert(key ~= nil)
local keys, key = {}, nil
repeat key = next(t, key)
keys[#keys+1] = key
until #keys >= 3 or key == nil
return keys
end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
local function get_assert_body (call_info) local function get_assert_body (call_info)
@ -556,17 +539,6 @@ local function determine_error_message (call_info, msg, condition)
assert(type(ast) == 'table') assert(type(ast) == 'table')
populate_ast_with_semantics(ast, call_info) populate_ast_with_semantics(ast, call_info)
local function is_l_value (ast)
print(ast.exp, ast.binop, ast.exp == 'OP' and ast.binop == 'DOT' )
if ast.exp == 'OP' and ast.binop == 'DOT' then
print 'Derp?'
return true
elseif ast.token == 'IDENTIFIER' then
return true
end
return false
end
-- Alternative more detailed formatting. -- Alternative more detailed formatting.
-- Identical to last message, but now with values of each involved -- Identical to last message, but now with values of each involved
-- identifier. -- identifier.
@ -589,11 +561,67 @@ local function determine_error_message (call_info, msg, condition)
return get_variable_and_prefix(token, call_info) return get_variable_and_prefix(token, call_info)
end end
local function fmt_number_value (value, relevant)
local relevant = relevant or {}
assert(type(value) == 'number')
assert(type(relevant) == 'table')
local l = { 'number', tonumber(value), base = 1 }
if value % 1 ~= 0 then l[1] = 'decimal number'
else l[1] = 'integer'
end
if relevant.sign then
l.base, l[0] = 0, (value > 0) and 'positive' or 'negative'
end
if relevant.remainder then
assert(type(relevant.remainder) == 'number')
if relevant.remainder == 1 then
-- Do nothing.
-- The remainder of a decimal number is obvious.
elseif relevant.remainder == 2 then
l[1] = (value % 2 == 0) and 'even number' or 'odd number'
else
l[3], l[4] = 'with remainder', value % relevant.remainder
end
end
return table.concat(l, ' ', l.base)
end
if not ast then return nil if not ast then return nil
elseif ast.exp == 'OP' and ast.binop == 'EQ' and ast[1].exp == 'CALL' and ast[1][1].value == type then elseif ast.exp == 'OP' and ast.binop == 'EQ' and ast[1].exp == 'CALL' and ast[1][1].value == type then
local prefix = fmt_prefix(ast[1][2], call_info) local prefix = fmt_prefix(ast[1][2], call_info)
msg[1], msg[2] = prefix, ('%s expected, but got %s'):format(ast[2].value, fmt_val_with_type(ast[1][2].value)) msg[1], msg[2] = prefix, ('%s expected, but got %s'):format(ast[2].value, fmt_val_with_type(ast[1][2].value))
elseif ast.exp == 'OP' and ast.binop == 'EQ' and ast[1].exp == 'OP' and ast[1].binop == 'MODULO' and ast[2].exp == 'NUMBER' then
-- a % b == c
local a = ast[1][1].value
local b = ast[1][2].value
local expect_remainder = ast[2].value
assert(type(a) == 'number')
assert(type(b) == 'number')
assert(type(expect_remainder) == 'number' and expect_remainder >= 0 and expect_remainder < b, 'Nonsensical desired remainder')
local expected_desc, relevant_attr = '???', { remainder = b }
if b == 2 and expect_remainder == 0 then
expected_desc = 'even number'
elseif b == 2 and expect_remainder == 1 then
expected_desc = 'odd number'
elseif b == 1 and expect_remainder == 0 then
expected_desc = 'integer'
elseif expect_remainder == 0 then
expected_desc = 'integer divisible by '..tostring(b)
else
expected_desc = 'integer with remainder '..expect_remainder..' when divided by '..tostring(b)
end
msg[1] = fmt_prefix(ast[1][1], call_info)
msg[2] = ('%s expected, but got %s'):format(expected_desc, fmt_number_value(ast[1][1].value, relevant_attr))
elseif ast.exp == 'OP' and ast.binop == 'EQ' then elseif ast.exp == 'OP' and ast.binop == 'EQ' then
local prefix = fmt_prefix(ast[1], call_info) local prefix = fmt_prefix(ast[1], call_info)
local gotten_value, expected_value = ast[1].value, ast[2].value local gotten_value, expected_value = ast[1].value, ast[2].value
@ -603,17 +631,30 @@ local function determine_error_message (call_info, msg, condition)
elseif ast.exp == 'OP' and ast.binop == 'NEQ' then elseif ast.exp == 'OP' and ast.binop == 'NEQ' then
local prefix = fmt_prefix(ast[1], call_info) local prefix = fmt_prefix(ast[1], call_info)
local gotten_value, expected_value = ast[1].value, ast[2].value local gotten_value, expected_value = ast[1].value, ast[2].value
msg[1], msg[2] = prefix, ('expected anything other than %s, but got %s'):format(fmt_val_with_type(expected_value), fmt_val(gotten_val)) msg[1], msg[2] = prefix, ('expected anything other than %s, but got %s'):format(fmt_val_with_type(expected_value), fmt_val(gotten_value))
elseif ast.exp == 'LVALUE' then elseif ast.exp == 'OP' and ast.binop == 'DOT' and is_l_value(ast[2]) then
local prefix = fmt_prefix(ast[1], call_info) local prefix = fmt_prefix(ast[2], call_info)
local gotten_value = ast[1].value local gotten_value = ast[2].value
msg[1], msg[2] = prefix, ('truthy expected, but got %s'):format(fmt_val(gotten_val)) local similar_keys, explain = similar_keys_in_table(ast[1].value, gotten_value)
if #similar_keys > 0 then
explain = (' close keys in %s include %s'):format(fmt_lvalue(ast[1]), fancy_fmt_seq(similar_keys))
else
explain = (' value of %s was %s'):format(fmt_lvalue(ast[1]), fmt_val(ast[1].value))
end
msg[1], msg[2] = prefix, ('value should occur as key in %s, but was %s.%s'):format(fmt_lvalue(ast[1], true), fmt_val(gotten_value), explain)
elseif CONSTANT_VALUE_TOKEN[ast.token] then elseif is_l_value(ast) then
local prefix = fmt_prefix(ast, call_info)
local gotten_value = ast.value
msg[1], msg[2] = prefix, ('truthy expected, but got %s'):format(fmt_val(gotten_value))
elseif CONSTANT_VALUE_TOKEN[ast.exp] then
local func_name = get_function_name(call_info) local func_name = get_function_name(call_info)
msg[1] = ('this assert will always fail, as it\'s body is `%s`. assumingly this should be an unreachable part of %s'):format(body_text, func_name) msg[1] = ('this assert will always fail, as it\'s body is `%s`. assumingly this should be an unreachable part of %s'):format(body_text, func_name)
elseif not ast.exp then
error(('[assert-gooder/internal]: Root node did not have expression type.'))
else else
error(('[assert-gooder/internal]: Unknown expression type %s'):format(ast.exp)) error(('[assert-gooder/internal]: Unknown expression type %s'):format(ast.exp))
end end

View File

@ -237,6 +237,15 @@ SUITE:addTest('table with more keys', function ()
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad of local \'obj\' (table similar to { a: string, b: number, ... } expected, but got { a = 42, b = "euclidian fantasyland", ... })', msg) assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad of local \'obj\' (table similar to { a: string, b: number, ... } expected, but got { a = 42, b = "euclidian fantasyland", ... })', msg)
end) end)
SUITE:addTest('table indexing', function ()
local _, msg = pcall(function ()
local obj = { hello = 4, world = 6, there = 2 }
local a = 'hullo'
assert(obj[a])
end)
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (value of a should be key in local "obj", but was "hullo". close keys in "obj" include "hello", "world" and "there")', msg)
end)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
SUITE:addTest('Number below something', function () SUITE:addTest('Number below something', function ()
@ -300,7 +309,15 @@ SUITE:addTest('Identify odd number', function ()
local a = 6 local a = 6
assert(a % 2 == 1) assert(a % 2 == 1)
end) end)
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (odd number expected, but got even number 5.21)', msg) assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (odd number expected, but got even number 6)', msg)
end)
SUITE:addTest('Divisible by 3', function ()
local _, msg = pcall(function ()
local a = 7
assert(a % 3 == 0)
end)
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (integer divisible by 3 expected, but got integer 7 with remainder 1)', msg)
end) end)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------