Module:LogicUtils: Difference between revisions
Appearance
No edit summary Tag: Manual revert |
support math style binary operators |
||
(16 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
local p = {} | 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 | end | ||
p.truth_table = function(frame, args) | 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 | 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