Improved modulo messages
This commit is contained in:
parent
f8447cdd39
commit
6c7c914da2
|
@ -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 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'
|
||||
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
|
||||
|
||||
local shunting_yard = require 'Parser'
|
||||
|
||||
local NO_PARSE_TOKENS = {
|
||||
FUNCTION = true,
|
||||
|
@ -186,7 +172,6 @@ local function parse (tokens)
|
|||
-- Postfix the tokens
|
||||
local postfix_tokens = shunting_yard(tokens, lua_expression_lang, NUM_ARITY)
|
||||
assert(type(postfix_tokens) == 'table')
|
||||
print(require'pretty'(postfix_tokens))
|
||||
|
||||
-- Create AST from postfix tokens
|
||||
local ast_stack = {}
|
||||
|
@ -251,40 +236,6 @@ local function parse (tokens)
|
|||
assert(#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
|
||||
|
||||
local function for_each_node_in_ast (ast, func)
|
||||
|
@ -303,12 +254,17 @@ end
|
|||
local function populate_ast_with_semantics (ast, info)
|
||||
assert(type(ast) == '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)
|
||||
if node.token == 'IDENTIFIER' then
|
||||
-- TODO: Variable scope, and is it in a function?
|
||||
if node.exp == 'IDENTIFIER' then
|
||||
node.value, node.scope, node.function_local = get_variable(node.text, info)
|
||||
elseif CONSTANT_VALUE_TOKEN[node.token] then
|
||||
node.value = CONSTANT_VALUE_TOKEN[node.token](node.text)
|
||||
elseif CONSTANT_VALUE_TOKEN[node.exp] then
|
||||
node.value = CONSTANT_VALUE_TOKEN[node.exp](node.text)
|
||||
elseif node.exp == 'OP' and node.binop == 'DOT' then
|
||||
assert(node[1].value and node[2].value)
|
||||
node.value = node[1].value[ node[2].value ] --TODO
|
||||
|
@ -416,7 +372,7 @@ end
|
|||
local function fmt_lvalue (node, with_scope)
|
||||
assert(type(node) == 'table')
|
||||
|
||||
if node.token == 'IDENTIFIER' then
|
||||
if node.exp == 'IDENTIFIER' then
|
||||
local base = node.text
|
||||
if with_scope then base = ('%s \'%s\''):format(node.scope, base) end
|
||||
return base, node.function_local
|
||||
|
@ -429,19 +385,6 @@ local function fmt_lvalue (node, with_scope)
|
|||
error 'Not implemented yet!'
|
||||
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)
|
||||
assert(type(ast) == 'table')
|
||||
--
|
||||
|
@ -476,7 +419,7 @@ local function fmt_table_with_type (val)
|
|||
|
||||
-- Conclude:
|
||||
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
|
||||
|
||||
|
@ -503,6 +446,46 @@ local function fmt_val_with_type (val)
|
|||
return type(val) .. ' ' .. fmt_val(val)
|
||||
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)
|
||||
|
@ -556,17 +539,6 @@ local function determine_error_message (call_info, msg, condition)
|
|||
assert(type(ast) == 'table')
|
||||
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.
|
||||
-- Identical to last message, but now with values of each involved
|
||||
-- identifier.
|
||||
|
@ -589,11 +561,67 @@ local function determine_error_message (call_info, msg, condition)
|
|||
return get_variable_and_prefix(token, call_info)
|
||||
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
|
||||
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)
|
||||
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
|
||||
local prefix = fmt_prefix(ast[1], call_info)
|
||||
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
|
||||
local prefix = fmt_prefix(ast[1], call_info)
|
||||
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
|
||||
local prefix = fmt_prefix(ast[1], call_info)
|
||||
local gotten_value = ast[1].value
|
||||
msg[1], msg[2] = prefix, ('truthy expected, but got %s'):format(fmt_val(gotten_val))
|
||||
elseif ast.exp == 'OP' and ast.binop == 'DOT' and is_l_value(ast[2]) then
|
||||
local prefix = fmt_prefix(ast[2], call_info)
|
||||
local gotten_value = ast[2].value
|
||||
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)
|
||||
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
|
||||
error(('[assert-gooder/internal]: Unknown expression type %s'):format(ast.exp))
|
||||
end
|
||||
|
|
|
@ -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)
|
||||
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 ()
|
||||
|
@ -300,7 +309,15 @@ SUITE:addTest('Identify odd number', function ()
|
|||
local a = 6
|
||||
assert(a % 2 == 1)
|
||||
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)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
Loading…
Reference in New Issue
Block a user