Misc
This commit is contained in:
parent
b1c6693f1f
commit
dcee4f6f95
|
@ -11,23 +11,23 @@ local function get_value_of_string (string_str)
|
||||||
end
|
end
|
||||||
|
|
||||||
local CONSTANT_VALUE_TOKEN = {
|
local CONSTANT_VALUE_TOKEN = {
|
||||||
NUMBER = tonumber,
|
NUMBER = tonumber,
|
||||||
STRING = get_value_of_string,
|
STRING = get_value_of_string,
|
||||||
TRUE = function() return true end,
|
TRUE = function() return true end,
|
||||||
FALSE = function() return false end,
|
FALSE = function() return false end,
|
||||||
NIL = function() return nil end
|
NIL = function() return nil end
|
||||||
}
|
}
|
||||||
|
|
||||||
local VALUE_TOKEN = { IDENTIFIER = true }
|
local VALUE_TOKEN = { IDENTIFIER = true }
|
||||||
for k in pairs(CONSTANT_VALUE_TOKEN) do VALUE_TOKEN[k] = true end
|
for k in pairs(CONSTANT_VALUE_TOKEN) do VALUE_TOKEN[k] = true end
|
||||||
|
|
||||||
local COMPARE_BINOP = {
|
local COMPARE_BINOP = {
|
||||||
EQ = true,
|
EQ = true,
|
||||||
NEQ = true,
|
NEQ = true,
|
||||||
LEQ = true,
|
LEQ = true,
|
||||||
GEQ = true,
|
GEQ = true,
|
||||||
LE = true,
|
LE = true,
|
||||||
GT = true,
|
GT = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
local function get_value_token (token)
|
local function get_value_token (token)
|
||||||
|
@ -39,6 +39,9 @@ local function get_value_token (token)
|
||||||
assert(false)
|
assert(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Parsing
|
||||||
|
|
||||||
local function parse (tokens)
|
local function parse (tokens)
|
||||||
-- TODO: Make a more general parser
|
-- TODO: Make a more general parser
|
||||||
assert(type(tokens) == 'table')
|
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
|
elseif #tokens == 3 and VALUE_TOKEN[tokens[1].token] and COMPARE_BINOP[tokens[2].token] and VALUE_TOKEN[tokens[3].token] then
|
||||||
return {
|
return {
|
||||||
exp = 'COMPARE',
|
exp = 'COMPARE',
|
||||||
binop = tokens[2].token,
|
binop = tokens[2].token,
|
||||||
left = get_value_token(tokens[1]),
|
left = get_value_token(tokens[1]),
|
||||||
right = get_value_token(tokens[3])
|
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
|
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 } }
|
return { exp = 'LVALUE', tokens[1].text, { exp = 'STRING', value = tokens[3].text } }
|
||||||
|
@ -65,95 +68,95 @@ local function parse (tokens)
|
||||||
elseif #tokens == 1 then
|
elseif #tokens == 1 then
|
||||||
return get_value_token(tokens[1])
|
return get_value_token(tokens[1])
|
||||||
else
|
else
|
||||||
error 'Unknown AST structure!'
|
io.stderr:write '[assert-gooder/internal]: Unknown AST structure!'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_assert_body_text (call_info)
|
local function populate_ast_with_semantics (node, env)
|
||||||
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_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
|
end
|
||||||
|
|
||||||
local function get_assert_body (call_info)
|
local function get_assert_body (call_info)
|
||||||
local text = get_assert_body_text(call_info)
|
local text = get_assert_body_text(call_info)
|
||||||
return lexer:lex(text), text
|
return lexer:lex(text), text
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_variable (var_name, info, level)
|
local function get_variable (var_name, info)
|
||||||
--
|
--
|
||||||
assert(type(var_name) == 'string')
|
assert(type(var_name) == 'string')
|
||||||
assert(type(info) == 'table')
|
assert(type(info) == 'table')
|
||||||
assert(type(level) == 'number')
|
|
||||||
|
|
||||||
-- Local
|
-- Local
|
||||||
local index, func = 0, info.func
|
if info.locals[var_name] then
|
||||||
repeat
|
local var_info = info.locals[var_name]
|
||||||
index = index + 1
|
return var_info[1], var_info[2] and ('argument #'..var_info[3]) or 'local', info.name or var_info[2] and ''
|
||||||
local name, val = debug.getlocal(level + 1, index)
|
end
|
||||||
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
|
|
||||||
|
|
||||||
-- Up-value
|
-- Up-value
|
||||||
local index = 0
|
local index = 0
|
||||||
repeat
|
repeat
|
||||||
index = index + 1
|
index = index + 1
|
||||||
local name, val = debug.getupvalue(func, index)
|
local name, val = debug.getupvalue(info.func, index)
|
||||||
if name == var_name then return val, 'upvalue' end
|
if name == var_name then return val, 'upvalue' end
|
||||||
until not name
|
until not name
|
||||||
|
|
||||||
-- Global
|
-- Global
|
||||||
return getfenv(func)[var_name], 'global'
|
return getfenv(info.func)[var_name], 'global'
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_function_name (call_info)
|
local function get_function_name (call_info)
|
||||||
--
|
--
|
||||||
if call_info.name then return string.format('\'%s\'', call_info.name) end
|
if call_info.name then return string.format('\'%s\'', call_info.name) end
|
||||||
--
|
--
|
||||||
local where = nil
|
local where = nil
|
||||||
if call_info.source:find '^@' then
|
if call_info.source:find '^@' then
|
||||||
where = 'at '..call_info.short_src..':'..call_info.linedefined
|
where = 'at '..call_info.short_src..':'..call_info.linedefined
|
||||||
elseif call_info.short_src:find '^%[string' then
|
elseif call_info.short_src:find '^%[string' then
|
||||||
where = 'from loaded string'
|
where = 'from loaded string'
|
||||||
else
|
else
|
||||||
error 'not yet implemented'
|
error 'not yet implemented'
|
||||||
end
|
end
|
||||||
--
|
--
|
||||||
return string.format('the anonymous function %s', where)
|
return string.format('the anonymous function %s', where)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function fmt_val (val)
|
local function fmt_val (val)
|
||||||
if type(val) == 'string' then
|
if type(val) == 'string' then
|
||||||
return string.format('%q', val)
|
return string.format('%q', val)
|
||||||
else
|
else
|
||||||
return tostring(val)
|
return tostring(val)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function fmt_lvalue (lvalue, var_scope)
|
local function fmt_lvalue (lvalue, var_scope)
|
||||||
|
@ -164,72 +167,71 @@ local function fmt_lvalue (lvalue, var_scope)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_variable_and_prefix (lvalue, call_info, level)
|
local function get_variable_and_prefix (lvalue, call_info)
|
||||||
assert(type(lvalue) == 'table' and lvalue.exp == 'LVALUE')
|
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)
|
||||||
local base_value, var_scope, in_func = get_variable(lvalue[1], call_info, level + 1)
|
-- Determine value of variable
|
||||||
-- Determine value of variable
|
local value = base_value
|
||||||
local value = base_value
|
for i = 2, #lvalue do value = value[lvalue[i].value] end
|
||||||
for i = 2, #lvalue do value = value[lvalue[i].value] end
|
--
|
||||||
--
|
local func_name = in_func and (' to '..get_function_name(call_info)) or ''
|
||||||
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)
|
||||||
return value, ('bad %s%s'):format(fmt_lvalue(lvalue, var_scope), func_name)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local PRIMITIVE_VALUES = {
|
local PRIMITIVE_VALUES = {
|
||||||
['nil'] = true,
|
['nil'] = true,
|
||||||
['boolean'] = true,
|
['boolean'] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
local COMPLEX_TYPES = {
|
local COMPLEX_TYPES = {
|
||||||
['table'] = true,
|
['table'] = true,
|
||||||
['userdata'] = true,
|
['userdata'] = true,
|
||||||
['cdata'] = true,
|
['cdata'] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
local function fmt_with_type (val)
|
local function fmt_with_type (val)
|
||||||
-- Primitive values ARE their type, and don't need the annotation.
|
-- Primitive values ARE their type, and don't need the annotation.
|
||||||
if PRIMITIVE_VALUES[type(val)] then return tostring(val) end
|
if PRIMITIVE_VALUES[type(val)] then return tostring(val) end
|
||||||
-- Complex types are already formatted with some type information.
|
-- Complex types are already formatted with some type information.
|
||||||
if COMPLEX_TYPES[type(val)] then return tostring(val) end
|
if COMPLEX_TYPES[type(val)] then return tostring(val) end
|
||||||
-- Numbers and string should have their types with them.
|
-- Numbers and string should have their types with them.
|
||||||
return type(val) .. ' ' .. fmt_val(val)
|
return type(val) .. ' ' .. fmt_val(val)
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
local function determine_error_message (call_info, msg, level, condition)
|
local function determine_error_message (call_info, msg, condition)
|
||||||
local tokens, body_text = get_assert_body(call_info)
|
local tokens, body_text = get_assert_body(call_info)
|
||||||
local ast = parse(tokens)
|
local ast = parse(tokens)
|
||||||
|
|
||||||
msg[1] = ('expression `%s` evaluated to %s'):format(body_text, condition)
|
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
|
local var_prefix = function(token) return get_variable_and_prefix(token, call_info) end
|
||||||
|
|
||||||
if not ast then return nil
|
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
|
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])
|
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))
|
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
|
elseif ast.exp == 'COMPARE' and ast.binop == 'EQ' then
|
||||||
local gotten_value, prefix = var_prefix(ast.left)
|
local gotten_value, prefix = var_prefix(ast.left)
|
||||||
local expected_value = ast.right.value
|
local expected_value = ast.right.value
|
||||||
local type_annotation = (type(expected_value) == type(gotten_value)) and '' or (' '..type(gotten_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))
|
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
|
elseif ast.exp == 'COMPARE' and ast.binop == 'NEQ' then
|
||||||
local gotten_val, prefix = var_prefix(ast.left)
|
local gotten_val, prefix = var_prefix(ast.left)
|
||||||
local expected_value = ast.right.value
|
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))
|
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
|
elseif ast.exp == 'LVALUE' then
|
||||||
local gotten_val, prefix = var_prefix(ast)
|
local gotten_val, prefix = var_prefix(ast)
|
||||||
msg[1] = ('%s (truthy expected, but got %s)'):format(prefix, fmt_val(gotten_val))
|
msg[1] = ('%s (truthy expected, but got %s)'):format(prefix, fmt_val(gotten_val))
|
||||||
|
|
||||||
elseif CONSTANT_VALUE_TOKEN[ast.exp] then
|
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)
|
||||||
|
|
||||||
else
|
else
|
||||||
error(('[assert-gooder/internal]: Unknown expression type %s'):format(ast.exp))
|
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
|
end
|
||||||
|
|
||||||
return function (condition)
|
return function (condition)
|
||||||
if condition then return condition end
|
if condition then return condition end
|
||||||
local level = 2
|
--
|
||||||
local call_info = debug.getinfo(level)
|
local level = 2
|
||||||
local msg_container = {''}
|
local call_info = debug.getinfo(level)
|
||||||
local success, internal_error_msg = pcall(determine_error_message, call_info, msg_container, level, condition)
|
call_info.locals = {}
|
||||||
-- Handle internal errors
|
for i = 1, math.huge do
|
||||||
if not success then
|
local name, value = debug.getlocal(level, i)
|
||||||
io.stderr:write(('[assert-gooder/internal]: Internal error occured while determining error message for calling assert:\n %s'):format(internal_error_msg))
|
if not name then break end
|
||||||
end
|
print(value, i <= call_info.nparams)
|
||||||
--
|
call_info.locals[name] = { value, i <= call_info.nparams, i }
|
||||||
error(('assertion failed! %s'):format(msg_container[1]), 2)
|
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
|
end
|
||||||
|
|
||||||
|
|
|
@ -180,7 +180,7 @@ SUITE:addTest('function as type argument', function ()
|
||||||
assert(type(f) == 'string')
|
assert(type(f) == 'string')
|
||||||
end)
|
end)
|
||||||
-- TODO: How do we test this?
|
-- 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)
|
end)
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
Loading…
Reference in New Issue
Block a user