diff --git a/assert-gooder.lua b/assert-gooder.lua index 51b727c..46af4f4 100644 --- a/assert-gooder.lua +++ b/assert-gooder.lua @@ -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 diff --git a/test/test_assert-gooder.lua b/test/test_assert-gooder.lua index a5b7f10..b0fde02 100644 --- a/test/test_assert-gooder.lua +++ b/test/test_assert-gooder.lua @@ -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) --------------------------------------------------------------------------------