Jump to content

Module:LogicUtils: Difference between revisions

From Logic World Wiki
No edit summary
support math style binary operators
 
(21 intermediate revisions by the same user not shown)
Line 1: Line 1:
local p = {}
local p = {}


function parse_io(kind, html, str)
local bit32 = require( 'bit32' )
local count = tonumber(str)
local names = {}


if count == nil then
function parse_io(kind, row, str)
-- str is a list of IO names
    local count = tonumber(str)
names = mw.text.split(str, "%s")
    local names = {}
else
-- str is a number
for i=1,count do
names[i] = string.format("%s %i", kind, i)
end
end


for i, v in ipairs(names) do
    if count == nil then
html = html.."<th> "..v.."</th>"
        -- str is a list of IO names
end
        names = mw.text.split(str, ",")
    else
return html
        -- str is a number
        if count == 1 then
            names[1] = kind
        else
            for i=1,count do
                names[i] = string.format("%s %i", kind, i)
            end
        end
    end
 
    for i, v in ipairs(names) do
        row:tag('th')
            :wikitext(v)
    end
 
    return #names
end
 
local function isBinOp(c)
    return c == "&" or c == "|" or c == "^" or c == "+" or c == "*"
end
 
local function letterToCell(c)
    return c:lower():byte() - 96
end
 
local function eval(expr, values)
    local stack = {}
 
    for _, c in pairs(expr) do
        if isBinOp(c) then
            local v = table.remove(stack)
            if v == nil then error("Invalid expression: missing operand") end
            local v2 = table.remove(stack)
            if v2 == nil then error("Invalid expression: missing operand") end
 
            if c == "&" or c == "*" then
                table.insert(stack, v and v2)
            elseif c == "|" or c == "+" then
                table.insert(stack, v or v2)
            elseif c == "^" then
                table.insert(stack, v ~= v2)
            end
        elseif c == "!" then
            local v = table.remove(stack)
            if v == nil then error("Invalid expression: missing operand after '!'") end
            table.insert(stack, not v)
        else
            local index = letterToCell(c) -- 'a' = 1, 'b' = 2, etc.
            if index then
                if values[index] == nil and not ignoreMissing then error("Value for variable '" .. c .. "' not provided") end
 
                table.insert(stack, values[index] or false)
            else
                error("Invalid character in expression: " .. c)
            end
        end
    end
 
    return stack[1]
end
 
local function evalAll(expr, num_vars, callback)
    local totalCombinations = 2 ^ num_vars
 
    for i = 0, totalCombinations - 1 do
        local values = {}
        for j = 1, num_vars do
            values[j] = bit32.band(i, bit32.lshift(1, num_vars - j)) ~= 0
        end
 
        local result = eval(expr, values)
        callback(values, result)
    end
end
 
local function algebraicToRPN(input)
    local output = {}
    local opstack = {}
 
    input:gsub("[^%s]", function(c)
        if isBinOp(c) then
            while opstack[#opstack] == "!" do
                table.insert(output, table.remove(opstack))
            end
 
            table.insert(opstack, c)
        elseif c == "!" or c == "(" then
            table.insert(opstack, c)
        elseif c == ")" then
            while #opstack > 0 do
                local op = table.remove(opstack)
                if op == "(" then
                    break
                end
                table.insert(output, op)
            end
        else
            table.insert(output, c)
        end
    end)
 
    -- Drain the operator stack to the output
    while #opstack > 0 do table.insert(output, table.remove(opstack)) end
 
    return output
end
 
local function renderCell(row, value)
    local style = "color:white; text-align:center;"
    if value == false or value == "0" then
        row:tag('td')
            :attr('style', 'background-color:#1f1e1e;'..style)
            :wikitext("0")
    elseif value == true or value == "1" then
        row:tag('td')
            :attr('style', 'background-color:#fd140f;'..style)
            :wikitext("1")
    else
        row:tag('td')
            :cssText("text-align:center")
            :wikitext(value)
    end
end
end


p.truth_table = function(frame, args)
p.truth_table = function(frame, args)
local args = args or frame:getParent().args
    local args = args or frame:getParent().args
local html = "<table class=\"wikitable\"><tr>"
 
    local tbl = mw.html.create('table')
        :addClass('wikitable')
 
    if args.caption ~= nil then
        tbl:tag('caption')
            :wikitext(args.caption)
    end
 
    local header = tbl:tag('tr')
    local nInputs = parse_io("Input", header, args.inputs)
    local nOutputs = parse_io("Output", header, args.outputs)
 
    local i = 1
    while args[i] ~= nil do
        local row = tbl:tag('tr')
 
        for token in string.gmatch(args[i], "[^%s]+") do
            renderCell(row, token)
        end
 
        i = i + 1
    end
 
    return tostring(tbl)
end
 
p.truth_table_auto = function(frame, args)
    local args = args or frame:getParent().args
 
    local tbl = mw.html.create('table')
        :addClass('wikitable')
 
    if args.caption ~= nil then
        tbl:tag('caption')
            :wikitext(args.caption)
    end
 
    local header = tbl:tag('tr')
    local nInputs = parse_io("Input", header, args.inputs)
    local nOutputs = parse_io("Output", header, args.outputs)
 
    local exprs = {}
    for i = 1,nOutputs do
        local exprStr = args["expr"..i]
        if exprStr == nil then error("Missing expression for output " .. i) end
        table.insert(exprs, algebraicToRPN(exprStr))
    end
 
    local totalCombinations = 2 ^ nInputs
    local values = {}
 
    for i = 0,totalCombinations - 1 do
        local row = tbl:tag('tr')
 
        for j = 1, nInputs do
            values[j] = bit32.band(i, bit32.lshift(1, nInputs - j)) ~= 0
            renderCell(row, values[j])
        end
 
        for _, expr in pairs(exprs) do
            renderCell(row, eval(expr, values))
        end
    end
 
    return tostring(tbl)
end
 
local function renderBinaryGraphHeader(tbl, tickCount)
    local row = tbl:tag('tr')
    row:tag('td')
    row:tag('td')
 
    for i = 0, tickCount do
        row:tag('td')
            :addClass('signal-header-cell')
            :wikitext(tostring(i))
    end
end
 
local function renderBinaryGraphSpacerRow(tbl, tickCount)
    local row = tbl:tag('tr')
        :addClass("signal-spacer-row")
    row:tag('td')
    row:tag('td')
 
    for i = 1, tickCount do
        row:tag('td')
            :addClass('signal-spacer-row-cell')
    end
end
 
local function renderBinaryGraphSignalRow(tbl, signal)
    local row = tbl:tag('tr')
        :addClass("signal-row")
    row:tag("td")
        :wikitext(signal.name)
 
    row:tag("td")
        :addClass("signal-spacer")
 
    local prevValue = nil
    for _, v in pairs(signal.values) do
        if v == "nil" then v = nil end
 
        local cell = row:tag("td")
            :addClass("signal-value-cell")
 
        if v == 1 then
            cell:addClass("signal-on-top")
        elseif v == 0 then
            cell:addClass("signal-on-bottom")
        end
 
        if prevValue ~= nil and v ~= nil and v ~= prevValue then
            cell:addClass("signal-on-left")
        end
 
        prevValue = v
    end
end
 
local function renderBinaryGraph(parent, signals)
    local tickCount = #signals[1].values
 
    local tbl = parent:tag('table')
 
    renderBinaryGraphHeader(tbl, tickCount)
 
    renderBinaryGraphSpacerRow(tbl, tickCount)
    for _, signal in pairs(signals) do
        renderBinaryGraphSignalRow(tbl, signal)
        renderBinaryGraphSpacerRow(tbl, tickCount)
    end
end
 
p.binary_signal_graph = function(frame, args)
    local args = args or frame:getParent().args
 
    local signalNames = mw.text.split(args.signals, ",")
    local signals = {}
 
    local container = mw.html.create('div')
        :addClass('signal-graph-table')
 
    for i = 1,#signalNames do
        local str = args["signal"..i]
        local values = {}
 
        for token in string.gmatch(str, "[01?]") do
            if token == "?" then
                table.insert(values, "nil") -- lua tables can't store nil
            else
                table.insert(values, tonumber(token))
            end
        end


html = parse_io("Input", html, args.inputs)
        table.insert(signals, { name = mw.text.trim(signalNames[i]), values = values })
html = parse_io("Output", html, args.outputs)
    end
html = html.."</tr>"
if args.caption ~= nil then
html = html.."<caption>"..args.caption.."</caption>"
end


local i = 1
    renderBinaryGraph(container, signals)
while args[i] ~= nil do
mw.log(args[i])
html = html.."<tr>"
for token in string.gmatch(args[i], "[^%s]+") do
if token == "0" then
html = html.."<td style=\"color:red; font-weight:bold\">0</td>"
elseif token == "1" then
html = html.."<td style=\"color:green; font-weight:bold\">1</td>"
else
html = html.."<td>"..token.."</td>"
end
end
html = html.."</tr>"
i = i + 1
end


html = html.."</table>"
    return tostring(container)
return html
end
end


return p
return p

Latest revision as of 18:57, 9 September 2025

Documentation for this module may be created at Module:LogicUtils/doc

local p = {}

local bit32 = require( 'bit32' )

function parse_io(kind, row, str)
    local count = tonumber(str)
    local names = {}

    if count == nil then
        -- str is a list of IO names
        names = mw.text.split(str, ",")
    else
        -- str is a number
        if count == 1 then
            names[1] = kind
        else
            for i=1,count do
                names[i] = string.format("%s %i", kind, i)
            end
        end
    end

    for i, v in ipairs(names) do
        row:tag('th')
            :wikitext(v)
    end

    return #names
end

local function isBinOp(c)
    return c == "&" or c == "|" or c == "^" or c == "+" or c == "*"
end

local function letterToCell(c)
    return c:lower():byte() - 96
end

local function eval(expr, values)
    local stack = {}

    for _, c in pairs(expr) do
        if isBinOp(c) then
            local v = table.remove(stack)
            if v == nil then error("Invalid expression: missing operand") end
            local v2 = table.remove(stack)
            if v2 == nil then error("Invalid expression: missing operand") end

            if c == "&" or c == "*" then
                table.insert(stack, v and v2)
            elseif c == "|" or c == "+" then
                table.insert(stack, v or v2)
            elseif c == "^" then
                table.insert(stack, v ~= v2)
            end
        elseif c == "!" then
            local v = table.remove(stack)
            if v == nil then error("Invalid expression: missing operand after '!'") end
            table.insert(stack, not v)
        else
            local index = letterToCell(c) -- 'a' = 1, 'b' = 2, etc.
            if index then
                if values[index] == nil and not ignoreMissing then error("Value for variable '" .. c .. "' not provided") end

                table.insert(stack, values[index] or false)
            else
                error("Invalid character in expression: " .. c)
            end
        end
    end

    return stack[1]
end

local function evalAll(expr, num_vars, callback)
    local totalCombinations = 2 ^ num_vars

    for i = 0, totalCombinations - 1 do
        local values = {}
        for j = 1, num_vars do
            values[j] = bit32.band(i, bit32.lshift(1, num_vars - j)) ~= 0
        end

        local result = eval(expr, values)
        callback(values, result)
    end
end

local function algebraicToRPN(input)
    local output = {}
    local opstack = {}

    input:gsub("[^%s]", function(c)
        if isBinOp(c) then
            while opstack[#opstack] == "!" do
                table.insert(output, table.remove(opstack))
            end

            table.insert(opstack, c)
        elseif c == "!" or c == "(" then
            table.insert(opstack, c)
        elseif c == ")" then
            while #opstack > 0 do
                local op = table.remove(opstack)
                if op == "(" then
                    break
                end
                table.insert(output, op)
            end
        else
            table.insert(output, c)
        end
    end)

    -- Drain the operator stack to the output
    while #opstack > 0 do table.insert(output, table.remove(opstack)) end

    return output
end

local function renderCell(row, value)
    local style = "color:white; text-align:center;"
    if value == false or value == "0" then
        row:tag('td')
            :attr('style', 'background-color:#1f1e1e;'..style)
            :wikitext("0")
    elseif value == true or value == "1" then
        row:tag('td')
            :attr('style', 'background-color:#fd140f;'..style)
            :wikitext("1")
    else
        row:tag('td')
            :cssText("text-align:center")
            :wikitext(value)
    end
end

p.truth_table = function(frame, args)
    local args = args or frame:getParent().args

    local tbl = mw.html.create('table')
        :addClass('wikitable')

    if args.caption ~= nil then
        tbl:tag('caption')
            :wikitext(args.caption)
    end

    local header = tbl:tag('tr')
    local nInputs = parse_io("Input", header, args.inputs)
    local nOutputs = parse_io("Output", header, args.outputs)

    local i = 1
    while args[i] ~= nil do
        local row = tbl:tag('tr')

        for token in string.gmatch(args[i], "[^%s]+") do
            renderCell(row, token)
        end

        i = i + 1
    end

    return tostring(tbl)
end

p.truth_table_auto = function(frame, args)
    local args = args or frame:getParent().args

    local tbl = mw.html.create('table')
        :addClass('wikitable')

    if args.caption ~= nil then
        tbl:tag('caption')
            :wikitext(args.caption)
    end

    local header = tbl:tag('tr')
    local nInputs = parse_io("Input", header, args.inputs)
    local nOutputs = parse_io("Output", header, args.outputs)

    local exprs = {}
    for i = 1,nOutputs do
        local exprStr = args["expr"..i]
        if exprStr == nil then error("Missing expression for output " .. i) end
        table.insert(exprs, algebraicToRPN(exprStr))
    end

    local totalCombinations = 2 ^ nInputs
    local values = {}

    for i = 0,totalCombinations - 1 do
        local row = tbl:tag('tr')

        for j = 1, nInputs do
            values[j] = bit32.band(i, bit32.lshift(1, nInputs - j)) ~= 0
            renderCell(row, values[j])
        end

        for _, expr in pairs(exprs) do
            renderCell(row, eval(expr, values))
        end
    end

    return tostring(tbl)
end

local function renderBinaryGraphHeader(tbl, tickCount)
    local row = tbl:tag('tr')
    row:tag('td')
    row:tag('td')

    for i = 0, tickCount do
        row:tag('td')
            :addClass('signal-header-cell')
            :wikitext(tostring(i))
    end
end

local function renderBinaryGraphSpacerRow(tbl, tickCount)
    local row = tbl:tag('tr')
        :addClass("signal-spacer-row")
    row:tag('td')
    row:tag('td')

    for i = 1, tickCount do
        row:tag('td')
            :addClass('signal-spacer-row-cell')
    end
end

local function renderBinaryGraphSignalRow(tbl, signal)
    local row = tbl:tag('tr')
        :addClass("signal-row")
    row:tag("td")
        :wikitext(signal.name)

    row:tag("td")
        :addClass("signal-spacer")

    local prevValue = nil
    for _, v in pairs(signal.values) do
        if v == "nil" then v = nil end

        local cell = row:tag("td")
            :addClass("signal-value-cell")

        if v == 1 then
            cell:addClass("signal-on-top")
        elseif v == 0 then
            cell:addClass("signal-on-bottom")
        end

        if prevValue ~= nil and v ~= nil and v ~= prevValue then
            cell:addClass("signal-on-left")
        end

        prevValue = v
    end
end

local function renderBinaryGraph(parent, signals)
    local tickCount = #signals[1].values

    local tbl = parent:tag('table')

    renderBinaryGraphHeader(tbl, tickCount)

    renderBinaryGraphSpacerRow(tbl, tickCount)
    for _, signal in pairs(signals) do
        renderBinaryGraphSignalRow(tbl, signal)
        renderBinaryGraphSpacerRow(tbl, tickCount)
    end
end

p.binary_signal_graph = function(frame, args)
    local args = args or frame:getParent().args

    local signalNames = mw.text.split(args.signals, ",")
    local signals = {}

    local container = mw.html.create('div')
        :addClass('signal-graph-table')

    for i = 1,#signalNames do
        local str = args["signal"..i]
        local values = {}

        for token in string.gmatch(str, "[01?]") do
            if token == "?" then
                table.insert(values, "nil") -- lua tables can't store nil
            else
                table.insert(values, tonumber(token))
            end
        end

        table.insert(signals, { name = mw.text.trim(signalNames[i]), values = values })
    end

    renderBinaryGraph(container, signals)

    return tostring(container)
end

return p