Executable Rust Code in Quarto

Quarto
Rust
Lua
Pandoc
Author

Galen Seilis

Published

July 26, 2024

Deprecated Output

I have turned off the Lua filter discussed in this post. It was running everytime I rendered/published the blog. I have copy-pasted the original output for reference.

The following is a Lua filter which looks through a qmd file for Rust code associated with {rust}, compiles that code using rustc, runs the compiled Rust program and collects its output, and inserts the output to be rendered by pandoc.

local io = require("io")
local os = require("os")
local tempfile = require("os").tmpname
local log_file

-- Function to initialize the log file
local function init_log()
  log_file = io.open("rust_executor_debug.log", "w")
end

-- Function to log messages to file and stderr
local function log(...)
  local args = {...}
  for i = 1, #args do
    args[i] = tostring(args[i])
  end
  local message = table.concat(args, " ")
  if log_file then
    log_file:write(message .. "\n")
    log_file:flush()
  end
  io.stderr:write(message .. "\n")
  io.stderr:flush()
end

-- Helper function to execute Rust code and return the output
local function execute_rust_code(code)
  local temp_file = tempfile() .. ".rs"
  log("Temporary Rust file:", temp_file)
  local source_file, err = io.open(temp_file, "w")
  if not source_file then
    log("Failed to create source file:", err)
    error("Failed to create source file: " .. err)
  end

  source_file:write(code)
  source_file:close()

  local temp_bin = tempfile()
  log("Temporary binary file:", temp_bin)

  local compile_command = "rustc " .. temp_file .. " -o " .. temp_bin .. " 2>&1"
  log("Compile Command:", compile_command)
  local compile_pipe = io.popen(compile_command)
  local compile_output = compile_pipe:read("*a")
  local compile_result = compile_pipe:close()

  if compile_result ~= true then
    os.remove(temp_file)
    log("Rust compilation failed. Output:", compile_output)
    error("Rust compilation failed. Output: " .. compile_output)
  end

  local exec_command = temp_bin .. " 2>&1"
  log("Exec Command:", exec_command)
  local exec_pipe = io.popen(exec_command)
  local output = exec_pipe:read("*a")
  exec_pipe:close()

  local ok, rm_err = pcall(function()
    os.remove(temp_file)
    os.remove(temp_bin)
  end)
  if not ok then
    log("Failed to clean up temporary files:", rm_err)
    error("Failed to clean up temporary files: " .. rm_err)
  end

  log("Output:", output)
  return output
end

local echo_global = true

function Meta(meta)
  if meta.echo ~= nil then
    echo_global = pandoc.utils.stringify(meta.echo) == "true"
  end
end

-- Lua filter function
function CodeBlock(elem)
  if not log_file then
    init_log()
  end

  local is_rust_code = elem.attr.classes:includes("{rust}")
  if is_rust_code then
    log("Processing Rust code block")
    local output = execute_rust_code(elem.text)
    output = output:gsub("%s+$", "")
    local blocks = {}

    if echo_global then
      -- Render Rust code as a formatted block
      table.insert(blocks, pandoc.CodeBlock(elem.text, {class="rust"}))
    end

    -- Always return the output
    table.insert(blocks, pandoc.Para(pandoc.Str(output)))

    return blocks
  else
    log("Skipping non-Rust code block")
  end
end

-- Ensure log file is closed properly at the end
function Pandoc(doc)
  if log_file then
    log_file:close()
  end
  return doc
end

Let’s try some examples.

Here is some Rust code that will be executed and rendered.

fn main() {
        println!("Galen Seilis is learning Rust!");
        println!("Time to get Rusty!");
}

Galen Seilis is learning Rust! Time to get Rusty!

Now let us try some Rust code that will not be executed.

fn main() {
    println!("Meow");
}

Now let us run a longer example from Rust by Example.

fn main() {
    // Integer addition
    println!("1 + 2 = {}", 1u32 + 2);

    // Integer subtraction
    println!("1 - 2 = {}", 1i32 - 2);
    // TODO ^ Try changing `1i32` to `1u32` to see why the type is important

    // Scientific notation
    println!("1e4 is {}, -2.5e-3 is {}", 1e4, -2.5e-3);

    // Short-circuiting boolean logic
    println!("true AND false is {}", true && false);
    println!("true OR false is {}", true || false);
    println!("NOT true is {}", !true);

    // Bitwise operations
    println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);
    println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);
    println!("0011 XOR 0101 is {:04b}\n\n\n", 0b0011u32 ^ 0b0101);
    println!("1 << 5 is {}", 1u32 << 5);
    println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);

    // Use underscores to improve readability!
    println!("One million is written as {}", 1_000_000u32);
}

1 + 2 = 3 1 - 2 = -1 1e4 is 10000, -2.5e-3 is -0.0025 true AND false is false true OR false is true NOT true is false 0011 AND 0101 is 0001 0011 OR 0101 is 0111 0011 XOR 0101 is 0110 1 << 5 is 32 0x80 >> 2 is 0x20 One million is written as 1000000

In the current state there are a couple of glaring issues I have with this implementation. The first is that Rust code blocks will be run regardless of whether echo: false is used. The second is that all the outputs are being rendered on a single, notwithstanding Quarto’s line wrapping.

There is also an enhancement which is desirable, which is to render other types of things from Rust that are not just plaintext. Instead of developing this kind of functionality myself, it would make sense to take a closer look at integrating tools such as the Evcxr Jupyter kernel.