From dcee4f6f95b077faa25c55a973fcd04be853abc4 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Tue, 7 Nov 2017 11:06:39 +0100 Subject: [PATCH] Misc --- assert-gooder.lua | 295 +++++++++++++++++++----------------- test/test_assert-gooder.lua | 2 +- 2 files changed, 154 insertions(+), 143 deletions(-) diff --git a/assert-gooder.lua b/assert-gooder.lua index a153539..a7b7bfc 100644 --- a/assert-gooder.lua +++ b/assert-gooder.lua @@ -11,23 +11,23 @@ local function get_value_of_string (string_str) end local CONSTANT_VALUE_TOKEN = { - NUMBER = tonumber, - STRING = get_value_of_string, - TRUE = function() return true end, - FALSE = function() return false end, - NIL = function() return nil end + NUMBER = tonumber, + STRING = get_value_of_string, + TRUE = function() return true end, + FALSE = function() return false end, + NIL = function() return nil end } local VALUE_TOKEN = { IDENTIFIER = true } for k in pairs(CONSTANT_VALUE_TOKEN) do VALUE_TOKEN[k] = true end local COMPARE_BINOP = { - EQ = true, - NEQ = true, - LEQ = true, - GEQ = true, - LE = true, - GT = true, + EQ = true, + NEQ = true, + LEQ = true, + GEQ = true, + LE = true, + GT = true, } local function get_value_token (token) @@ -39,6 +39,9 @@ local function get_value_token (token) assert(false) end +-------------------------------------------------------------------------------- +-- Parsing + local function parse (tokens) -- TODO: Make a more general parser assert(type(tokens) == 'table') @@ -53,10 +56,10 @@ local function parse (tokens) } 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, - left = get_value_token(tokens[1]), - right = get_value_token(tokens[3]) + exp = 'COMPARE', + binop = tokens[2].token, + left = get_value_token(tokens[1]), + right = get_value_token(tokens[3]) } 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 } } @@ -65,95 +68,95 @@ local function parse (tokens) elseif #tokens == 1 then return get_value_token(tokens[1]) else - error 'Unknown AST structure!' + io.stderr:write '[assert-gooder/internal]: Unknown AST structure!' end end -local function get_assert_body_text (call_info) - if call_info.what == 'Lua' or call_info.what == 'main' then - -- Find filetext - local filetext = nil - if call_info.source:find '^@' then - local f = io.open(call_info.short_src, 'r') - filetext = f:read '*all' - f:close() - elseif call_info.short_src:find '^%[string' then - filetext = call_info.source - else - error 'Not implemented yet!' - end - -- Get lines - local filetext = filetext .. '\n' - local lines_after, line_i = {}, 0 - for line in filetext:gmatch '([^\r\n]*)[\r\n]' do - line_i = line_i + 1 - if call_info.currentline == line_i then - lines_after[#lines_after+1] = line - end - end - -- Find body exclusively. - return table.concat(lines_after, '\n'):match('assert%s*(%b())'):sub(2, -2) - end +local function populate_ast_with_semantics (node, env) - error 'Not implemented yet!' +end + +-------------------------------------------------------------------------------- + +local function get_assert_body_text (call_info) + if call_info.what == 'Lua' or call_info.what == 'main' then + -- Find filetext + local filetext = nil + if call_info.source:find '^@' then + local f = io.open(call_info.short_src, 'r') + filetext = f:read '*all' + f:close() + elseif call_info.short_src:find '^%[string' then + filetext = call_info.source + else + error 'Not implemented yet!' + end + -- Get lines + local filetext = filetext .. '\n' + local lines_after, line_i = {}, 0 + for line in filetext:gmatch '([^\r\n]*)[\r\n]' do + line_i = line_i + 1 + if call_info.currentline == line_i then + lines_after[#lines_after+1] = line + end + end + -- Find body exclusively. + return table.concat(lines_after, '\n'):match('assert%s*(%b())'):sub(2, -2) + end + + error 'Not implemented yet!' end local function get_assert_body (call_info) - local text = get_assert_body_text(call_info) - return lexer:lex(text), text + local text = get_assert_body_text(call_info) + return lexer:lex(text), text end -local function get_variable (var_name, info, level) - -- - assert(type(var_name) == 'string') - assert(type(info) == 'table') - assert(type(level) == 'number') +local function get_variable (var_name, info) + -- + assert(type(var_name) == 'string') + assert(type(info) == 'table') - -- Local - local index, func = 0, info.func - repeat - index = index + 1 - local name, val = debug.getlocal(level + 1, index) - if name == var_name then - local is_par = index <= info.nparams - return val, is_par and ('argument #'..index) or 'local', info.name or is_par and '' - end - until not name + -- Local + if info.locals[var_name] then + local var_info = info.locals[var_name] + return var_info[1], var_info[2] and ('argument #'..var_info[3]) or 'local', info.name or var_info[2] and '' + end - -- Up-value - local index = 0 - repeat - index = index + 1 - local name, val = debug.getupvalue(func, index) - if name == var_name then return val, 'upvalue' end - until not name + -- Up-value + local index = 0 + repeat + index = index + 1 + local name, val = debug.getupvalue(info.func, index) + if name == var_name then return val, 'upvalue' end + until not name - -- Global - return getfenv(func)[var_name], 'global' + -- Global + return getfenv(info.func)[var_name], 'global' end local function get_function_name (call_info) - -- - if call_info.name then return string.format('\'%s\'', call_info.name) end - -- - local where = nil - if call_info.source:find '^@' then - where = 'at '..call_info.short_src..':'..call_info.linedefined - elseif call_info.short_src:find '^%[string' then - where = 'from loaded string' - else - error 'not yet implemented' - end - -- - return string.format('the anonymous function %s', where) + -- + if call_info.name then return string.format('\'%s\'', call_info.name) end + -- + local where = nil + if call_info.source:find '^@' then + where = 'at '..call_info.short_src..':'..call_info.linedefined + elseif call_info.short_src:find '^%[string' then + where = 'from loaded string' + else + error 'not yet implemented' + end + -- + return string.format('the anonymous function %s', where) end local function fmt_val (val) - if type(val) == 'string' then - return string.format('%q', val) - else - return tostring(val) - end + if type(val) == 'string' then + return string.format('%q', val) + else + return tostring(val) + end end local function fmt_lvalue (lvalue, var_scope) @@ -164,72 +167,71 @@ local function fmt_lvalue (lvalue, var_scope) end end -local function get_variable_and_prefix (lvalue, call_info, level) - assert(type(lvalue) == 'table' and lvalue.exp == 'LVALUE') - assert(type(level) == 'number') - -- - local base_value, var_scope, in_func = get_variable(lvalue[1], call_info, level + 1) - -- Determine value of variable - local value = base_value - for i = 2, #lvalue do value = value[lvalue[i].value] end - -- - local func_name = in_func and (' to '..get_function_name(call_info)) or '' - return value, ('bad %s%s'):format(fmt_lvalue(lvalue, var_scope), func_name) +local function get_variable_and_prefix (lvalue, call_info) + assert(type(lvalue) == 'table' and lvalue.exp == 'LVALUE') + -- + local base_value, var_scope, in_func = get_variable(lvalue[1], call_info) + -- Determine value of variable + local value = base_value + for i = 2, #lvalue do value = value[lvalue[i].value] end + -- + local func_name = in_func and (' to '..get_function_name(call_info)) or '' + return value, ('bad %s%s'):format(fmt_lvalue(lvalue, var_scope), func_name) end local PRIMITIVE_VALUES = { - ['nil'] = true, - ['boolean'] = true, + ['nil'] = true, + ['boolean'] = true, } local COMPLEX_TYPES = { - ['table'] = true, - ['userdata'] = true, - ['cdata'] = true, + ['table'] = true, + ['userdata'] = true, + ['cdata'] = true, } local function fmt_with_type (val) - -- Primitive values ARE their type, and don't need the annotation. - if PRIMITIVE_VALUES[type(val)] then return tostring(val) end - -- Complex types are already formatted with some type information. - if COMPLEX_TYPES[type(val)] then return tostring(val) end - -- Numbers and string should have their types with them. - return type(val) .. ' ' .. fmt_val(val) + -- Primitive values ARE their type, and don't need the annotation. + if PRIMITIVE_VALUES[type(val)] then return tostring(val) end + -- Complex types are already formatted with some type information. + if COMPLEX_TYPES[type(val)] then return tostring(val) end + -- Numbers and string should have their types with them. + return type(val) .. ' ' .. fmt_val(val) end -------------------------------------------------------------------------------- -local function determine_error_message (call_info, msg, level, condition) - local tokens, body_text = get_assert_body(call_info) - local ast = parse(tokens) +local function determine_error_message (call_info, msg, condition) + local tokens, body_text = get_assert_body(call_info) + local ast = parse(tokens) - msg[1] = ('expression `%s` evaluated to %s'):format(body_text, condition) - local var_prefix = function(token) return get_variable_and_prefix(token, call_info, level + 2) end + msg[1] = ('expression `%s` evaluated to %s'):format(body_text, condition) + local var_prefix = function(token) return get_variable_and_prefix(token, call_info) end - if not ast then return nil - elseif ast.exp == 'COMPARE' and ast.binop == 'EQ' and ast.left.exp == 'CALL' and ast.left.callee.exp == 'LVALUE' and ast.left.callee[1] == 'type' then - local gotten_val, prefix = var_prefix(ast.left[1]) - msg[1] = ('%s (%s expected, but got %s: %s)'):format(prefix, ast.right.value, type(gotten_val), fmt_val(gotten_val)) + if not ast then return nil + elseif ast.exp == 'COMPARE' and ast.binop == 'EQ' and ast.left.exp == 'CALL' and ast.left.callee.exp == 'LVALUE' and ast.left.callee[1] == 'type' then + local gotten_val, prefix = var_prefix(ast.left[1]) + msg[1] = ('%s (%s expected, but got %s: %s)'):format(prefix, ast.right.value, type(gotten_val), fmt_val(gotten_val)) - elseif ast.exp == 'COMPARE' and ast.binop == 'EQ' then - local gotten_value, prefix = var_prefix(ast.left) - local expected_value = ast.right.value - local type_annotation = (type(expected_value) == type(gotten_value)) and '' or (' '..type(gotten_value)) - msg[1] = ('%s (%s expected, but got%s: %s)'):format(prefix, fmt_with_type(expected_value), type_annotation, fmt_val(gotten_value)) + elseif ast.exp == 'COMPARE' and ast.binop == 'EQ' then + local gotten_value, prefix = var_prefix(ast.left) + local expected_value = ast.right.value + local type_annotation = (type(expected_value) == type(gotten_value)) and '' or (' '..type(gotten_value)) + msg[1] = ('%s (%s expected, but got%s: %s)'):format(prefix, fmt_with_type(expected_value), type_annotation, fmt_val(gotten_value)) - elseif ast.exp == 'COMPARE' and ast.binop == 'NEQ' then - local gotten_val, prefix = var_prefix(ast.left) - local expected_value = ast.right.value - msg[1] = ('%s (expected anything other than %s, but got %s)'):format(prefix, fmt_with_type(expected_value), fmt_val(gotten_val)) + elseif ast.exp == 'COMPARE' and ast.binop == 'NEQ' then + local gotten_val, prefix = var_prefix(ast.left) + local expected_value = ast.right.value + msg[1] = ('%s (expected anything other than %s, but got %s)'):format(prefix, fmt_with_type(expected_value), fmt_val(gotten_val)) - elseif ast.exp == 'LVALUE' then - local gotten_val, prefix = var_prefix(ast) - msg[1] = ('%s (truthy expected, but got %s)'):format(prefix, fmt_val(gotten_val)) + elseif ast.exp == 'LVALUE' then + local gotten_val, prefix = var_prefix(ast) + msg[1] = ('%s (truthy expected, but got %s)'):format(prefix, fmt_val(gotten_val)) 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) + 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) else error(('[assert-gooder/internal]: Unknown expression type %s'):format(ast.exp)) @@ -237,16 +239,25 @@ local function determine_error_message (call_info, msg, level, condition) end return function (condition) - if condition then return condition end - local level = 2 - local call_info = debug.getinfo(level) - local msg_container = {''} - local success, internal_error_msg = pcall(determine_error_message, call_info, msg_container, level, condition) - -- Handle internal errors - if not success then - io.stderr:write(('[assert-gooder/internal]: Internal error occured while determining error message for calling assert:\n %s'):format(internal_error_msg)) - end - -- - error(('assertion failed! %s'):format(msg_container[1]), 2) + if condition then return condition end + -- + local level = 2 + local call_info = debug.getinfo(level) + call_info.locals = {} + for i = 1, math.huge do + local name, value = debug.getlocal(level, i) + if not name then break end + print(value, i <= call_info.nparams) + call_info.locals[name] = { value, i <= call_info.nparams, i } + end + -- + local msg_container = {''} + local success, internal_error_msg = pcall(determine_error_message, call_info, msg_container, condition) + -- Handle internal errors + if not success then + io.stderr:write(('[assert-gooder/internal]: Internal error occured while determining error message for calling assert:\n %s'):format(internal_error_msg)) + end + -- + error(('assertion failed! %s'):format(msg_container[1]), 2) end diff --git a/test/test_assert-gooder.lua b/test/test_assert-gooder.lua index b97d772..8790d9a 100644 --- a/test/test_assert-gooder.lua +++ b/test/test_assert-gooder.lua @@ -180,7 +180,7 @@ SUITE:addTest('function as type argument', function () assert(type(f) == 'string') end) -- TODO: How do we test this? - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'f\' (expected string, but got function)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'f\' (string expected, but got function)', msg) end) --------------------------------------------------------------------------------