#let truth-table(outputs: (), inputs: none) = {
  let variables = calc.max(..outputs.map(output => calc.ceil(calc.log(output.at(1).len()) / calc.log(2))))

  if inputs == none {
    inputs = ($a$, $b$, $c$, $d$).slice(0, variables)
  }

  assert(outputs.len() >= 1, message: "There has to be at least one output")
  assert(inputs.len() == variables, message: "There aren't enough variables to label")

  let num-to-bin(x, digits) = {
    let bits = ()

    while x != 0 {
      bits.push(calc.rem(x, 2))

      x = calc.floor(x / 2)
    }

    range(digits).map(x => bits.at(digits - x - 1, default: 0))
  }

  table(
    columns: (auto,) * (variables + outputs.len()),
    stroke: (x, y) => (
      left: if x == variables { 1pt } else if x > 0 { 0.5pt },
      top: if y == 1 { 1pt } else if y > 0 { 0.5pt },
    ),
    inset: 4pt,
    if inputs != none { table.header(..inputs.map(x => [#x]), ..outputs.map(((x, _)) => [#x])) },
    ..range(calc.pow(2, variables))
      .map(x => (..num-to-bin(x, variables).map(y => [#y]), ..outputs.map(((_, y)) => [#y.at(x, default: [])])))
      .flatten()
  )
}