Merge pull request #3900 from ZyX-I/inf-nan-string

Make it possible to eval() all floating-point values dumped by string()
This commit is contained in:
Justin M. Keyes 2016-02-28 11:48:54 -05:00
commit 0c2ba7554f
7 changed files with 259 additions and 11 deletions

View File

@ -206,6 +206,12 @@ if(MSVC)
else()
add_definitions(-Wall -Wextra -pedantic -Wno-unused-parameter
-Wstrict-prototypes -std=gnu99)
# On FreeBSD 64 math.h uses unguarded C11 extension, which taints clang
# 3.4.1 used there.
if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
add_definitions(-Wno-c11-extensions)
endif()
endif()
if(MINGW)

View File

@ -395,7 +395,8 @@ endfunction
""
" Dump floating-point value.
function s:msgpack_dump_float(v) abort
return string(type(a:v) == type({}) ? a:v._VAL : a:v)
return substitute(string(type(a:v) == type({}) ? a:v._VAL : a:v),
\'\V\^\(-\)\?str2float(''\(inf\|nan\)'')\$', '\1\2', '')
endfunction
""

View File

@ -6238,12 +6238,22 @@ string({expr}) Return {expr} converted to a String. If {expr} is a Number,
{expr} type result ~
String 'string'
Number 123
Float 123.123456 or 1.123456e8
Funcref function('name')
Float 123.123456 or 1.123456e8 or
`str2float('inf')`
Funcref `function('name')`
List [item, item]
Dictionary {key: value, key: value}
Note that in String values the ' character is doubled.
Also see |strtrans()|.
Note 2: Output format is mostly compatible with YAML, except
for infinite and NaN floating-point values representations
which use |str2float()|. Strings are also dumped literally,
only single quote is escaped, which does not allow using YAML
for parsing back binary strings (including text when
'encoding' is not UTF-8). |eval()| should always work for
strings and floats though and this is the only official
method, use |msgpackdump()| or |json_encode()| if you need to
share data with other application.
*strlen()*
strlen({expr}) The result is a Number, which is the length of the String

View File

@ -99,6 +99,8 @@ are always available and may be used simultaneously in separate plugins. The
Same thing applies to |string()| (though it uses construct like
"{E724@level}"), but this is not reliable because |string()| continues to
error out.
4. Stringifyed infinite and NaN values now use |str2float()| and can be evaled
back.
Viminfo text files were replaced with binary (messagepack) ShaDa files.
Additional differences:

View File

@ -4010,12 +4010,22 @@ eval6 (
* When either side is a float the result is a float.
*/
if (use_float) {
if (op == '*')
if (op == '*') {
f1 = f1 * f2;
else if (op == '/') {
/* We rely on the floating point library to handle divide
* by zero to result in "inf" and not a crash. */
f1 = f2 != 0 ? f1 / f2 : INFINITY;
} else if (op == '/') {
// Division by zero triggers error from AddressSanitizer
f1 = (f2 == 0
? (
#ifdef NAN
f1 == 0
? NAN
:
#endif
(f1 > 0
? INFINITY
: -INFINITY)
)
: f1 / f2);
} else {
EMSG(_("E804: Cannot use '%' with Float"));
return FAIL;
@ -6848,9 +6858,25 @@ vim_to_msgpack_error_ret: \
#define CONV_FLOAT(flt) \
do { \
char numbuf[NUMBUFLEN]; \
vim_snprintf(numbuf, NUMBUFLEN - 1, "%g", (flt)); \
ga_concat(gap, (char_u *) numbuf); \
const float_T flt_ = (flt); \
switch (fpclassify(flt_)) { \
case FP_NAN: { \
ga_concat(gap, (char_u *) "str2float('nan')"); \
break; \
} \
case FP_INFINITE: { \
if (flt_ < 0) { \
ga_append(gap, '-'); \
} \
ga_concat(gap, (char_u *) "str2float('inf')"); \
break; \
} \
default: { \
char numbuf[NUMBUFLEN]; \
vim_snprintf(numbuf, NUMBUFLEN - 1, "%g", flt_); \
ga_concat(gap, (char_u *) numbuf); \
} \
} \
} while (0)
#define CONV_FUNC(fun) \

View File

@ -0,0 +1,28 @@
local helpers = require('test.functional.helpers')
local eq = helpers.eq
local eval = helpers.eval
local clear = helpers.clear
describe('Division operator', function()
before_each(clear)
it('returns infinity on {positive}/0.0', function()
eq('str2float(\'inf\')', eval('string(1.0/0.0)'))
eq('str2float(\'inf\')', eval('string(1.0e-100/0.0)'))
eq('str2float(\'inf\')', eval('string(1.0e+100/0.0)'))
eq('str2float(\'inf\')', eval('string((1.0/0.0)/0.0)'))
end)
it('returns -infinity on {negative}/0.0', function()
eq('-str2float(\'inf\')', eval('string((-1.0)/0.0)'))
eq('-str2float(\'inf\')', eval('string((-1.0e-100)/0.0)'))
eq('-str2float(\'inf\')', eval('string((-1.0e+100)/0.0)'))
eq('-str2float(\'inf\')', eval('string((-1.0/0.0)/0.0)'))
end)
it('returns NaN on 0.0/0.0', function()
eq('str2float(\'nan\')', eval('string(0.0/0.0)'))
eq('str2float(\'nan\')', eval('string(-(0.0/0.0))'))
eq('str2float(\'nan\')', eval('string((-0.0)/0.0)'))
end)
end)

View File

@ -0,0 +1,175 @@
local helpers = require('test.functional.helpers')
local clear = helpers.clear
local eq = helpers.eq
local command = helpers.command
local meths = helpers.meths
local eval = helpers.eval
local exc_exec = helpers.exc_exec
local redir_exec = helpers.redir_exec
local funcs = helpers.funcs
local write_file = helpers.write_file
describe('string() function', function()
before_each(clear)
describe('used to represent floating-point values', function()
it('dumps NaN values', function()
eq('str2float(\'nan\')', eval('string(str2float(\'nan\'))'))
end)
it('dumps infinite values', function()
eq('str2float(\'inf\')', eval('string(str2float(\'inf\'))'))
eq('-str2float(\'inf\')', eval('string(str2float(\'-inf\'))'))
end)
it('dumps regular values', function()
eq('1.5', funcs.string(1.5))
eq('1.56e-20', funcs.string(1.56000e-020))
eq('0.0', eval('string(0.0)'))
end)
it('dumps values with at most six digits after the decimal point',
function()
eq('1.234568e-20', funcs.string(1.23456789123456789123456789e-020))
eq('1.234568', funcs.string(1.23456789123456789123456789))
end)
it('dumps values with at most seven digits before the decimal point',
function()
eq('1234567.891235', funcs.string(1234567.89123456789123456789))
eq('1.234568e7', funcs.string(12345678.9123456789123456789))
end)
it('dumps negative values', function()
eq('-1.5', funcs.string(-1.5))
eq('-1.56e-20', funcs.string(-1.56000e-020))
eq('-1.234568e-20', funcs.string(-1.23456789123456789123456789e-020))
eq('-1.234568', funcs.string(-1.23456789123456789123456789))
eq('-1234567.891235', funcs.string(-1234567.89123456789123456789))
eq('-1.234568e7', funcs.string(-12345678.9123456789123456789))
end)
end)
describe('used to represent numbers', function()
it('dumps regular values', function()
eq('0', funcs.string(0))
eq('-1', funcs.string(-1))
eq('1', funcs.string(1))
end)
it('dumps large values', function()
eq('2147483647', funcs.string(2^31-1))
eq('-2147483648', funcs.string(-2^31))
end)
end)
describe('used to represent strings', function()
it('dumps regular strings', function()
eq('\'test\'', funcs.string('test'))
end)
it('dumps empty strings', function()
eq('\'\'', funcs.string(''))
end)
it('dumps strings with \' inside', function()
eq('\'\'\'\'\'\'\'\'', funcs.string('\'\'\''))
eq('\'a\'\'b\'\'\'\'\'', funcs.string('a\'b\'\''))
eq('\'\'\'b\'\'\'\'d\'', funcs.string('\'b\'\'d'))
eq('\'a\'\'b\'\'c\'\'d\'', funcs.string('a\'b\'c\'d'))
end)
end)
describe('used to represent funcrefs', function()
local fname = 'Xtest-functional-eval-string_spec-fref-script.vim'
before_each(function()
write_file(fname, [[
function Test1()
endfunction
function s:Test2()
endfunction
function g:Test3()
endfunction
let g:Test2_f = function('s:Test2')
]])
command('source ' .. fname)
end)
after_each(function()
os.remove(fname)
end)
it('dumps references to built-in functions', function()
eq('function(\'function\')', eval('string(function("function"))'))
end)
it('dumps references to user functions', function()
eq('function(\'Test1\')', eval('string(function("Test1"))'))
eq('function(\'g:Test3\')', eval('string(function("g:Test3"))'))
end)
it('dumps references to script functions', function()
eq('function(\'<SNR>1_Test2\')', eval('string(Test2_f)'))
end)
end)
describe('used to represent lists', function()
it('dumps empty list', function()
eq('[]', funcs.string({}))
end)
it('dumps nested lists', function()
eq('[[[[[]]]]]', funcs.string({{{{{}}}}}))
end)
it('dumps nested non-empty lists', function()
eq('[1, [[3, [[5], 4]], 2]]', funcs.string({1, {{3, {{5}, 4}}, 2}}))
end)
it('errors when dumping recursive lists', function()
meths.set_var('l', {})
eval('add(l, l)')
eq('Vim(echo):E724: unable to correctly dump variable with self-referencing container',
exc_exec('echo string(l)'))
end)
it('dumps recursive lists despite the error', function()
meths.set_var('l', {})
eval('add(l, l)')
eq('\nE724: unable to correctly dump variable with self-referencing container\n[{E724@0}]',
redir_exec('echo string(l)'))
eq('\nE724: unable to correctly dump variable with self-referencing container\n[[{E724@1}]]',
redir_exec('echo string([l])'))
end)
end)
describe('used to represent dictionaries', function()
it('dumps empty dictionary', function()
eq('{}', eval('string({})'))
end)
it('dumps non-empty dictionary', function()
eq('{\'t\'\'est\': 1}', funcs.string({['t\'est']=1}))
end)
it('errors when dumping recursive dictionaries', function()
meths.set_var('d', {d=1})
eval('extend(d, {"d": d})')
eq('Vim(echo):E724: unable to correctly dump variable with self-referencing container',
exc_exec('echo string(d)'))
end)
it('dumps recursive dictionaries despite the error', function()
meths.set_var('d', {d=1})
eval('extend(d, {"d": d})')
eq('\nE724: unable to correctly dump variable with self-referencing container\n{\'d\': {E724@0}}',
redir_exec('echo string(d)'))
eq('\nE724: unable to correctly dump variable with self-referencing container\n{\'out\': {\'d\': {E724@1}}}',
redir_exec('echo string({"out": d})'))
end)
end)
end)