fix: preserve exact numeric literals and restore safe AST constant folding - #247
bkkpro1980 wants to merge 4 commits into
Conversation
levno-710
left a comment
There was a problem hiding this comment.
I found two issues that I think need fixing before this merges:
-
The new
parseHexaccumulates into a double and can round a large literal differently from Lua itself. For example,print(string.format("%.0f", 0x2944075f548d8d9))prints185844359999576288on both Lua 5.1 and Luau, but the output produced with theVmifypreset prints185844359999576256.Vmifyrebuilds the number node without itsrawtext, so preserving the spelling in the unparser does not cover this path. Could you fix the conversion and add an end-to-end case throughVmify? -
Test 7 in
tests/numeric-precision.luahas an incorrect expected result. It printsTEST 7 FAILEDon both Lua 5.1 and Luau. Please correct the expected result.
|
I read your comment just now and I'll look into it. Thank you |
|
I think the issues have been fixed. I ran tests and it came out fine. I also tested obfuscation on some of my Lua scripts and those seem to work as intended after the fixes. |
Summary
This PR resolves IEEE-754 precision loss and representation drift for numeric constants while restoring compile-time AST constant folding for safe expressions.
Background & Problem
Precision Loss on Unparsing:
unparser.luapreviously reconstructed numeric literals usingtostring(val), which could lose significant digits when converting parsed numeric values back into source text. This affected exact integers near the 53-bit boundary (2^53 - 1) and high-precision decimal floats, while large hexadecimal constants were re-emitted in lossy or decimal forms.Large Hexadecimal Literal Parsing: Relying on
tonumber(source, 16)could parse large hexadecimal literals incorrectly on affected 32-bit environments.Premature / Unsafe Constant Folding: Disabling constant folding globally via
simplify = falseprevented precision loss, but also disabled standard compile-time simplifications such as2 + 3,not false, string concatenation, and condition pruning. Conversely, unconstrained folding could evaluate 32-bit cryptographic multiplications such as0x811C9DC5 * 0x01000193using host doubles, potentially corrupting the value before 32-bit modulo extraction.Changes
Source Provenance for Number Literals (
NumberExpression.raw):Ast.NumberExpressionto store the original token slice (raw).Parser:expressionLiteralnow passestk.sourcetoAst.NumberExpression.unparser.luaemitsexpression.rawdirectly when available. ForLua51targets, digit separators (_) are stripped and binary literals (0b...) fall back to canonical numeric formatting; forLuaUtargets, the original literal is emitted verbatim.raw = niland use canonical%.0fformatting for safe integers and%.17gfor other numeric values.Tokenizer Hardening (
tokenizer.lua):tonumberwith digit-accumulation parsers (parseHex,parseBinary) inTokenizer:numberto avoid platform-dependent integer parsing behavior.0xand0b.Safe AST Constant Folding (
ast.lua):+,-,*,/,%, unary-,^) only fold when operands and the result are safe integers.lhs % rhs == 0) with non-zero divisors; dyadic fractions and inexact divisions remain as AST expressions.Ast.OrExpressionandAst.AndExpressionnow return the selected operand AST node directly, preserving node identity,.rawprovenance, and Lua operand-return semantics.canCompareEqualityConstantsandcanCompareOrderConstantsto prevent folding comparisons that would error at runtime in Lua (such as1 < "2") and prevent unsafe equality folds on large numeric values...), string length (#), and logicalnot.Regression Test Suite (
tests/numeric-precision.lua):0x100000000), maximum safe integers (0x1FFFFFFFFFFFFF), decimal boundary arithmetic at2^53, safe arithmetic folding, large cryptographic multiplication preservation, dyadic fractions,and/orsemantics, boundary comparisons, and 16-digit float preservation.Verification
Ran the full test suite across all built-in presets (
Medium,Minify,Strong,Vmify,Weak) on bothlua5.1andluauinside Docker: