Lawyers count on Tritium to be a stable desktop drafting environment.
And Rust's borrow-checker gives us a lot of assurances toward that goal, especially as we're able to avoid memory corruption in safe Rust.
But stability also means avoiding crashing or corrupting data for other reasons.
To that end, here I'll share two ideas that have helped in Tritium's development.
Don't Panic!
Like segfaults in C/C++, panics in Rust are great during development. They crash the program and tell you exactly where things went wrong.
But if you're deploying an application to non-technical users, crashing the program, even if it has arrived at an unexpected state, may not be the best approach. Causing the program instead to "glitch" but continue may allow the user to save their work and restart it gracefully.
We still want to capture the stack trace at the moment of such glitch when running in debug mode. That allow us to debug the issue on-the-spot rather than attempt a replication.
In VS Code, enabling breakpoints on all exceptions is especially helpful, allowing us to jump into the debugger at the panic if we're running the application with the debugger attached.
Here's a simple macro I've found useful:
#[cfg(debug_assertions)]
macro_rules! panic_or_return {
() => {
panic!("The program encountered an unexpected state.")
};
}
#[cfg(not(debug_assertions))]
macro_rules! panic_or_return {
() => {
return
};
}
Now, instead of using the ?
operator, expect()
or unwrap()
on
Result
and Option
return values, we can use blocks like this:
pub(crate) fn insert_section_break_at(
docx: &mut crate::docx::Docx,
cursor: crate::cursor::Cursor,
) -> crate::cursor::Cursor {
let new_cursor = crate::ops::insert_paragraph_at(docx, cursor);
let Some(paragraph) = new_cursor.paragraph_mut_in(docx) else {
panic_or_return_default!();
};
paragraph.add_section_properties(crate::docx::SectionProperties::default());
new_cursor.with_section_i(new_cursor.section_i + 1)
}
This code looks to insert a section break into a Word document at a given Cursor
point. First, it
adds a paragraph which cannot fail and returns a new Cursor
to the beginning of the
paragraph.
The Cursor.paragraph_mut_in
object wants to return a mutable
reference to the Paragraph
it's pointing at. This method can fail. That's because
seeking that Paragraph
requires looping over Tritium's custom AST
for Word documents under the hood. If a match isn't found, it returns None
.
But that None
really should never happen. It's a programming error if it does since we only
instantiate Cursor
objects from within Tritium code.
But if it does occur, rather than panic
at the end of
that paragraph_mut_in
loop, returning None
allows Tritium to survive the error.
Instead of dying, it
causes the new section break appear at the Cursor
default. Literally that
would mean a new section break at the beginning of the document. That's just a glitch. Such a glitch will be
obvious to the user and a
quick Ctrl+Z
will get them back to the prior state. That allows them to save their work and restart
if necessary.
Could the macro be more informative with a message? Yes. But keeping it
simple allows it to replace the ?
operator where you're certain the
Result
or Option
will unwrap.
This strategy should only be used where returning early or default would be obvious to the user
and be
consistent with the upstream handling of an Err
or None
value. Heavy reliance on this
sort of "fail but continue" pattern should be seen as a form of code
smell that hides a deeper underlying problem. That underlying problem is known for Tritium:
partial implementation of a large specification. Over time, Tritium should rely on this pattern with less
frequency. It provides stability in the meantime.
Limit Recursion
When you're dealing with wild data adhering to a specification as big as the Docx spec, you need to be prepared to approach established limits.
Laying out a Word document, for example, with nested objects lends itself to recursive strategies. But unbounded recursion can overflow the stack and crash the program. Tritium's layout code encountered such a problem. Again, we didn't want to throw away the useful information of the crash, but we'd rather avoid that in a release build running on a lawyer's desktop.
Instead of recursion, we use a bounded iterative pattern that looks something like this:
fn fit_paragraph(
...
) {
for _ in 0..crate::config::Limits::MAX_PAGES_PER_PARAGRAPH {
...
if let Some(edge) = self.position_paragraph(&mut paragraph, typeset_section) {
let next = paragraph.split_off_at(edge);
self.finalize_paragraph(section, frame, paragraph);
self.begin_page(section, frame);
let Some(next_paragraph) = next else {
return;
};
paragraph = next_paragraph;
...
} else {
self.finalize_paragraph(section, frame, paragraph);
return;
}
}
#[cfg(debug_assertions)]
panic!("Limits::MAX_PAGES_PER_PARAGRAPH exceeded.")
}
In a debug build, we'll hit the limit with a helpful panic
message. In a release build, Tritium
will silently truncate the paragraph to MAX_PAGES_PER_PARAGRAPH
pages. The key is to ensure that
number is high enough that it would be an obvious "glitch". That again allows the user
to recover, undo and restart if necessary.
Hope these help.