Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/prometheus/cff/expressions/number.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ return function(self, expression, funcDepth, numReturns)
for i = 1, numReturns do
regs[i] = self:allocRegister();
if i == 1 then
self:addStatement(self:setRegister(scope, regs[i], Ast.NumberExpression(expression.value)), {regs[i]}, {}, false);
if type(expression.value) == "number" and expression.value == 0 and 1 / expression.value < 0 then
-- Keep negative zero out of native Lua 5.1 numeric constants.
-- The register read makes this runtime negation distinct from a -0 literal.
self:addStatement(self:setRegister(scope, regs[i], Ast.NumberExpression(0)), {regs[i]}, {}, false);
self:addStatement(self:setRegister(scope, regs[i], Ast.NegateExpression(self:register(scope, regs[i]))), {regs[i]}, {regs[i]}, false);
else
self:addStatement(self:setRegister(scope, regs[i], Ast.NumberExpression(expression.value)), {regs[i]}, {}, false);
end
else
self:addStatement(self:setRegister(scope, regs[i], Ast.NilExpression()), {regs[i]}, {}, false);
end
Expand Down
4 changes: 1 addition & 3 deletions src/prometheus/cff/register.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

local Ast = require("prometheus.ast");
local constants = require("prometheus.cff.constants");
local randomStrings = require("prometheus.randomStrings");

local MAX_REGS = constants.MAX_REGS;

Expand Down Expand Up @@ -226,9 +225,8 @@ return function(Compiler)

function Compiler:setPos(scope, val)
if not val then
local v = Ast.IndexExpression(self:env(scope), randomStrings.randomStringNode(math.random(12, 14)));
scope:addReferenceToHigherScope(self.containerFuncScope, self.posVar);
return Ast.AssignmentStatement({Ast.AssignmentVariable(self.containerFuncScope, self.posVar)}, {v});
return Ast.AssignmentStatement({Ast.AssignmentVariable(self.containerFuncScope, self.posVar)}, {Ast.NilExpression()});
end
scope:addReferenceToHigherScope(self.containerFuncScope, self.posVar);
return Ast.AssignmentStatement({Ast.AssignmentVariable(self.containerFuncScope, self.posVar)}, {Ast.NumberExpression(val) or Ast.NilExpression()});
Expand Down
18 changes: 16 additions & 2 deletions src/prometheus/cff/statements/assignment.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
local Ast = require("prometheus.ast");
local AstKind = Ast.AstKind;

local function isSideEffectFreeLiteral(expression)
local kind = expression.kind;
return kind == AstKind.NilExpression or kind == AstKind.BooleanExpression or
kind == AstKind.NumberExpression or kind == AstKind.StringExpression;
end

return function(self, statement, funcDepth)
local scope = self.activeBlock.scope;
local exprregs = {};
Expand Down Expand Up @@ -44,19 +50,27 @@ return function(self, statement, funcDepth)
table.insert(exprregs, reg);
end
else
if statement.lhs[i] or expr.kind == AstKind.FunctionCallExpression or expr.kind == AstKind.PassSelfFunctionCallExpression then
if statement.lhs[i] then
local reg = self:compileExpression(expr, funcDepth, 1)[1];
if(self:isVarRegister(reg)) then
local ro = reg;
reg = self:allocRegister(false);
self:addStatement(self:copyRegisters(scope, {reg}, {ro}), {reg}, {ro}, false);
end
table.insert(exprregs, reg);
else
-- Lua evaluates every RHS expression, including values that
-- have no corresponding assignment target.
if not isSideEffectFreeLiteral(expr) then
local reg = self:compileExpression(expr, funcDepth, 1)[1];
self:freeRegister(reg, false);
end
end
end
end

for i, primaryExpr in ipairs(statement.lhs) do
for i = #statement.lhs, 1, -1 do
local primaryExpr = statement.lhs[i];
if primaryExpr.kind == AstKind.AssignmentVariable then
if primaryExpr.scope.isGlobal then
local tmpReg = self:allocRegister(false);
Expand Down
15 changes: 14 additions & 1 deletion src/prometheus/cff/statements/local_variable_declaration.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
local Ast = require("prometheus.ast");
local AstKind = Ast.AstKind;

local function isSideEffectFreeLiteral(expression)
local kind = expression.kind;
return kind == AstKind.NilExpression or kind == AstKind.BooleanExpression or
kind == AstKind.NumberExpression or kind == AstKind.StringExpression;
end

return function(self, statement, funcDepth)
local scope = self.activeBlock.scope;
local exprregs = {};
Expand All @@ -17,9 +23,16 @@ return function(self, statement, funcDepth)
table.insert(exprregs, reg);
end
else
if statement.ids[i] or expr.kind == AstKind.FunctionCallExpression or expr.kind == AstKind.PassSelfFunctionCallExpression then
if statement.ids[i] then
local reg = self:compileExpression(expr, funcDepth, 1)[1];
table.insert(exprregs, reg);
else
-- Initializers past the declared names are discarded, but
-- Lua still evaluates them for their effects and errors.
if not isSideEffectFreeLiteral(expr) then
local reg = self:compileExpression(expr, funcDepth, 1)[1];
self:freeRegister(reg, false);
end
end
end
end
Expand Down
18 changes: 15 additions & 3 deletions src/prometheus/tokenizer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -445,15 +445,27 @@ function Tokenizer:multiLineString()
-- Multiline String
-- Parse String to Closing bracket but also consider that the count of equal signs must be the same

-- Skip Leading newline if existing
-- Lua normalizes CR, LF, CRLF, and LFCR line breaks to LF in long strings.
local function normalizedNewline(char)
local nextChar = peek(self)
if (char == "\r" and nextChar == "\n") or (char == "\n" and nextChar == "\r") then
self.index = self.index + 1
end
return "\n"
end

-- Skip one leading newline sequence, if present.
self.index = self.index + 1;
if(is(self, "\n")) then
self.index = self.index + 1;
if(is(self, "\n") or is(self, "\r")) then
normalizedNewline(get(self))
end

local value = "";
while true do
local char = get(self);
if char == "\n" or char == "\r" then
char = normalizedNewline(char)
end
if(char == ']') then
local eqCount2 = 0;
while(is(self, "=")) do
Expand Down
5 changes: 5 additions & 0 deletions src/prometheus/vm/compiler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ end
function Compiler:load(value)
local register = self:alloc()
if value == nil then self:emit('NIL', register)
elseif type(value) == 'number' and value == 0 and 1 / value < 0 then
-- Lua 5.1 merges signed-zero literals in one native function's constants.
-- Materialize the sign with a VM operation so it cannot be lost in emitted data.
self:emit('LOAD', register, self:constant(0))
self:emit('NEG', register, register)
else self:emit('LOAD', register, self:constant(value)) end
return register
end
Expand Down
44 changes: 44 additions & 0 deletions tests/cff-assignment-store-order.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-- @runtime lua51
-- Parallel assignments evaluate targets and values first, then store right to left.
local value
value, value = 10, 20
assert(value == 10)

local writes = {}
local proxy = setmetatable({}, {
__newindex = function(_, key, assigned)
writes[#writes + 1] = key .. "=" .. assigned
end,
})
proxy.a, proxy.b, proxy.c = 1, 2, 3
assert(table.concat(writes, ",") == "c=3,b=2,a=1")

local repeated = {}
repeated[1], repeated[1], repeated[1] = 10, 20, 30
assert(repeated[1] == 10)

-- Indexed targets retain their table/key before later target and RHS writes.
local tableValue = {old = 0}
local saved = tableValue
local key = "old"
tableValue, tableValue[key] = {}, 5
assert(tableValue.old == nil and saved.old == 5)

local order = {}
local target = {}
local counter = 0
local function targetKey()
order[#order + 1] = "lhs"
counter = counter + 1
return counter
end
local function rightValue()
order[#order + 1] = "rhs"
counter = counter + 1
return counter
end
target[targetKey()], target[targetKey()] = rightValue(), rightValue()
assert(table.concat(order, ",") == "lhs,lhs,rhs,rhs")
assert(target[1] == 3 and target[2] == 4)

print("OK")
49 changes: 49 additions & 0 deletions tests/cff-discarded-expressions.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- @runtime lua51
-- Discarded expressions in assignment and local initializer lists still run.
local calls = 0
local object = setmetatable({}, {
__index = function()
calls = calls + 1
return 17
end,
__add = function()
calls = calls + 10
return 23
end,
})

local first
first = 1, object.missing
assert(calls == 1)

first = 1, object + object
assert(calls == 11)

local ok = pcall(function()
first = 1, nil + {}
end)
assert(not ok, "discarded operator error must propagate")

local localValue = 1, object.missing
assert(localValue == 1 and calls == 12)

-- A discarded global read must still consult the active function environment.
local originalSetfenv = setfenv
local reads = 0
local env = setmetatable({}, {
__index = function(_, key)
if key == "unusedGlobal" then
reads = reads + 1
return true
end
error("unexpected global lookup: " .. tostring(key), 0)
end,
})
local function readDiscardedGlobal()
local value = 1, unusedGlobal
return value
end
originalSetfenv(readDiscardedGlobal, env)
assert(readDiscardedGlobal() == 1 and reads == 1)

print("OK")
49 changes: 49 additions & 0 deletions tests/cff-terminal-position-environment.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- @runtime lua51
-- Ending Cff dispatch must not perform a random global/environment lookup.
local originalSetfenv = setfenv
local originalAssert = assert
local function strictEnvironment()
return setmetatable({}, {
__index = function(_, key)
error("unexpected environment lookup: " .. tostring(key), 0)
end,
})
end

local function explicitReturn()
return 42
end
originalSetfenv(explicitReturn, strictEnvironment())
originalAssert(explicitReturn() == 42)

local function fallthrough()
local value = 1
end
originalSetfenv(fallthrough, strictEnvironment())
originalAssert(fallthrough() == nil)

local function outer()
local function nested()
return 7
end
return nested
end
originalSetfenv(outer, strictEnvironment())
local nested = outer()
originalAssert(nested() == 7)

-- Also verify the no-error case does not make an extra lookup.
local lookups = {}
local loggingEnv = setmetatable({}, {
__index = function(_, key)
lookups[#lookups + 1] = key
return nil
end,
})
local function loggedReturn()
return 9
end
originalSetfenv(loggedReturn, loggingEnv)
originalAssert(loggedReturn() == 9 and #lookups == 0)

print("OK")
29 changes: 29 additions & 0 deletions tests/harness/long-string-newlines.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require("src.prometheus") -- Sets the repository module path used by tokenizer dependencies.
local Tokenizer = require("prometheus.tokenizer")

local function check(body, expected)
local source = "[=[" .. body .. "]=]"
local tokenizer = Tokenizer:new({LuaVersion = "Lua51"})
tokenizer:append(source)
local token = tokenizer:next()
assert(token.kind == Tokenizer.TokenKind.String)
assert(token.value == expected, string.format("expected %q, got %q", expected, token.value))
assert(token.startPos == 0 and token.endPos == #source, "token positions must use raw source bytes")
assert(token.source == source, "token source must retain original newline bytes")
end

-- Lua drops one initial newline sequence, for every accepted line ending.
for _, newline in ipairs({"\n", "\r", "\r\n", "\n\r"}) do
check(newline .. "body", "body")
end

-- Internal newline sequences normalize to LF; equal adjacent bytes stay distinct.
check("a\nb\rc\r\nd\n\re", "a\nb\nc\nd\ne")
check("a\r\rb\n\nc", "a\n\nb\n\nc")

-- A newline immediately before the closing delimiter is still part of the value.
for _, newline in ipairs({"\n", "\r", "\r\n", "\n\r"}) do
check("body" .. newline, "body\n")
end

print("Long string newline normalization checks passed")
69 changes: 69 additions & 0 deletions tests/harness/vm-signed-zero.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
local Prometheus = require('src.prometheus')
Prometheus.Logger.logLevel = Prometheus.Logger.LogLevel.Error
local Parser = require('prometheus.parser')
local Compiler = require('prometheus.vm.compiler')
local Emit = require('prometheus.vm.emit')
local opcodes = require('prometheus.vm.opcodes')

local source = [[
local function negativeIntegerZero() return -0 end
local function negativeDecimalZero() return -0.0 end
local function negativeProductZero() return 0 * -1 end
local function positiveZero() return 0 end
local function nestedNegativeZero()
local function inner() return -0 end
return inner()
end
assert(1 / negativeIntegerZero() == -math.huge)
assert(1 / negativeDecimalZero() == -math.huge)
assert(1 / negativeProductZero() == -math.huge)
assert(1 / nestedNegativeZero() == -math.huge)
assert(1 / positiveZero() == math.huge)
]]

local proto = Compiler:compile(Parser:new({LuaVersion = 'Lua51'}):parse(source))
local negativeZeroLoads = 0
local function checkPrototype(value)
for _, constant in ipairs(value.constants) do
assert(not (type(constant) == 'number' and constant == 0 and 1 / constant < 0),
'VM prototype must not contain a negative-zero literal')
end
for _, instruction in ipairs(value.code) do
if instruction[1] == opcodes.NEG then negativeZeroLoads = negativeZeroLoads + 1 end
end
for _, child in ipairs(value.children) do checkPrototype(child) end
end
checkPrototype(proto)
assert(negativeZeroLoads >= 4, 'negative-zero literals must lower to VM NEG in their prototypes')

assert(loadstring(Emit.source(proto)))()
print('VM signed-zero checks passed')

for _, preset in ipairs({'Cff', 'Weak'}) do
for _, seed in ipairs({1, 2, 17}) do
local config = require('src.presets')[preset]
config.Seed = seed
local generated = Prometheus.Pipeline:fromConfig(config):apply(source)
assert(loadstring(generated))()
end
end

-- Keep more than MAX_REGS locals live so the negative-zero register spills.
local locals, checks = {}, {}
for i = 1, 130 do
locals[i] = 'local value' .. i .. '=' .. i
checks[i] = 'assert(value' .. i .. '==' .. i .. ')'
end
local spillSource = table.concat(locals, '\n') .. [[
local negative = -0
assert(1 / negative == -math.huge)
]] .. table.concat(checks, '\n')
for _, preset in ipairs({'Cff', 'Weak'}) do
for _, seed in ipairs({1, 2, 17}) do
local config = require('src.presets')[preset]
config.Seed = seed
local generated = Prometheus.Pipeline:fromConfig(config):apply(spillSource)
assert(loadstring(generated))()
end
end
print('Cff signed-zero and spill checks passed')
Loading
Loading