Skip to content

Midstall/ironstyle

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

IronStyle

IronStyle is Midstall's Zig coding philosophy. Midstall designs silicon and builds the software that runs on it, from reset vectors and interrupt handlers up through kernels, compositors, and application libraries. IronStyle reflects that full range: rules are written from a hardware-and-software reality, not from a purely hosted perspective.

Manifesto

Midstall ships firmware, kernels, compositors, and tooling, all in Zig. Safety, performance, and clarity matter at every level of that stack, but the constraints at the bare-metal level are different from those at the application level. IronStyle names those differences explicitly rather than papering over them.

IronStyle was inspired by TigerStyle by the TigerBeetle team, and we credit it honestly as prior art. TigerStyle is tuned for one high-integrity database. Midstall spans a much broader range of targets and lifetimes, so priorities diverge where they need to. Safety, performance, and clarity are universal engineering values, not anyone's property. One honest acknowledgment, then we walk our own road.

Two core values anchor everything else:

Do it right the first time. Upfront rigor costs less than rework. Think through invariants, write tests for the cases you expect and the ones you are suspicious about, and add a regression test for every fixed bug so it does not come back.

Performance over beauty, on the measured hot path only. Off the hot path, clarity wins. This is not license to write ugly code everywhere. It is a scoped rule that applies after measurement identifies the bottleneck. It never buys undefined behavior: no skipped input validation, no @setRuntimeSafety(false) in place of correct logic. Ugly-but-fast is acceptable on a proven hot path with a comment explaining why. Unsafe-and-fast is not acceptable anywhere.

The Backbone Rule

Assert on programmer errors. Recover from runtime faults. Never assume I/O succeeds.

Programmer errors are broken invariants, impossible states, and logic bugs that only happen when the programmer made a mistake. Surface them loudly in development with assertions so they are caught before shipping, not silently mishandled in production.

Runtime faults are things outside the programmer's control: protocol errors, malformed input, transient hardware faults, resource exhaustion, and all I/O. These are recovered from, not asserted away. Long-lived systems, compositors, firmware, kernels, degrade gracefully rather than crash on a recoverable fault.

Recovery is never silent. Every recovered fault is logged, counted, or surfaced in a way that keeps the system observable. Silent failure is not recovery. It is a bug that hides itself.

Hosted vs Embedded

Whether the compilation target is known when writing the code is a first-class axis in IronStyle. Many rules split on it.

Hosted / applications (target unknown at authoring time). Use std.process.Init and std.Io injection. Entry is:

pub fn main(init: std.process.Init) !void {
    // Set up I/O from init, pass it down rather than calling std.io.getStdOut directly.
}

Injecting I/O rather than reaching for global handles makes libraries testable and lets multiple instances run in the same process without threadvar or a shared global.

Embedded (target known). Hardwire to the hardware. No std.process, no hosted std.Io. Entry is the reset vector or export fn _start. Context is a statically-allocated singleton passed by pointer to every function that needs it.

The one sanctioned exception to the no-globals rule: interrupt handlers are dispatched to fixed addresses by hardware and cannot take a context parameter. Minimal, volatile, documented ISR-to-mainloop state is allowed. Keep it minimal and document why it must be global.

Three Pillars

Safety

Error handling and assertions | Testing | Memory

  • Use assertions for programmer errors. Use error returns for runtime faults.
  • Push every check as far left as it goes. If an invariant is decidable at compile time (buffer sizes, alignment, power-of-two capacities, field-width sums), enforce it with a comptime block and @compileError so a bad build fails instead of a device. A comptime assert costs nothing on the target and cannot be stripped by a release build the way std.debug.assert can. Confirm each one can actually fail: an assert you never saw fail is an assumption.
  • Use exhaustive switches. Do not use else where the compiler could catch a missed case.
  • No undefined behavior. The backbone rule applies: assert on logic errors, recover from input errors, never assume I/O succeeds.
  • Bound everything: loops have maximum iteration counts where practical, allocations have explicit limits, recursion is bounded or replaced with iteration.
  • Never trust external or user input. Sanitize or bound it before use. Out-of-bounds or malformed input is a recoverable runtime fault, not a programmer error. Use std.enums.fromInt to convert untrusted integers to enums (returns null on unknown values), and never use @enumFromInt on an untrusted value, which is undefined behavior for an invalid tag. Do arithmetic on untrusted values with checked operations (std.math.add, @addWithOverflow), not with operators that panic on overflow.
  • Write meaningful tests: every test asserts a specific, non-trivial fact and is named for that fact. Assert the value returned, not merely that the call did not error. A test that only proves the code did not crash pins nothing. Cover the cases you expect and the cases you are suspicious about (boundaries, wraparound, empty, full), and add a regression test for every fixed bug the moment you fix it. See testing.

Performance

Memory | Cross-compilation

  • Design for the most constrained target in the codebase. Hosted convenience is not a reason to add overhead that will not exist on a microcontroller.
  • No hidden allocations. Every allocation is explicit and routed through a passed-in allocator.
  • Deterministic allocation: allocate at init or startup, zero allocation in the steady-state hot path.
  • Measure before optimizing. Use napkin math to sanity-check whether an optimization matters before writing it.
  • Prefer @Vector and SIMD intrinsics where the target supports them and the gain is measurable.
  • Performance over beauty applies only after measurement identifies the hot path, and never buys undefined behavior.

Clarity

Portability | Pure Zig | Standard library | Comments

  • Reach for the standard library before writing your own. std already covers the things most often reinvented: std.mem (compare, copy, find, readInt), std.ascii (classify, fold case), std.math (clamp, divCeil, checked arithmetic), std.sort, and std.fmt. The stdlib is tested, portable across every target, and correct at the edges a hand-rolled version forgets (length checks, overflow, byte order). Reinvent only when the profiler proved the stdlib form is too slow on the hot path, with a comment saying what you measured.
  • Name things for what they are. Abbreviate only when the abbreviation is unambiguous in context.
  • Zero-dependency builds. zig build is the only tool required to build and test the project. No generated code from external tools, no C dependencies, no system libraries unless the target demands it.
  • Comment the why, not the what, and only where it earns its place. Not everything needs a comment: a name like openFile already says what it does, so a comment restating that is noise that rots. Spend comments where a reader would otherwise be stuck: a magic value's datasheet source, an ordering the hardware demands, a unit or invariant the types do not capture. Prefer a better name over a comment, and keep comments true, a wrong comment is worse than none. See comments.
  • Use the filesystem as a namespace. Placing b.zig inside a/ beside a.zig lets you expose a.b, but this is not automatic: a.zig must re-export with pub const b = @import("a/b.zig");. Flat layout gives unrelated top-level symbols. Use nesting deliberately to build concise, hierarchical symbol tables.
  • Prefer comptime generics over redundant monomorphic functions. Write fn write(comptime T: type, ...) void once rather than write8, write16, write32 as separate functions that do the same thing with a different type.
  • Diffs should be readable. Prefer small, focused changes over large reshufflings.
  • Control layout with the trailing comma. A trailing comma in a struct literal or argument list makes zig fmt expand it one field per line. Drop it and zig fmt collapses it onto one line. Reach for the multi-line form when it reads better (a std.Target.Query with cpu_arch, os_tag, and abi), the single line when it does not. Pick the layout deliberately instead of fighting the formatter.

Porting C to Zig

Midstall reimplements C libraries in Zig often, and a port is a reimplementation, not a transliteration. It is the best chance to make the code correct, idiomatic, and portable, so take it:

  1. Fix the bugs, do not port them. A reimplementation should correct the original's missing bounds checks, overflows, and undefined behavior, not copy them across. Deviate deliberately and write down why.
  2. Keep the interface familiar. Someone who knows the C library should recognize the Zig one: same roles, same argument meaning, same names where they are well known. Familiar in concept, idiomatic in mechanism.
  3. Do not sacrifice Zig's features for parity. Use error unions, slices, optionals, comptime, and tagged unions rather than mirroring C's int-error-code and void * shape. Matching C's capabilities plus Zig's is the goal.
  4. Introduce portability. Shed the original's platform assumptions: explicit byte order, no libc, injected I/O, no static globals, so the port runs freestanding and supports multiple instances.

And before any of it, check that std does not already provide the thing (a port of what std ships is just reinvention). See porting.

Deep Dives

  • docs/cross-compilation.md - target-agnostic code, comptime target detection, freestanding builds.
  • docs/portability.md - uniform frontend, target-selected backend, injection, and the no-globals rationale.
  • docs/memory.md - allocators, ownership by lifetime, pointer safety, and OOM by target.
  • docs/pure-zig.md - no libc, no C dependencies, no external tools in build or test, freestanding test runners.
  • docs/stdlib.md - standard library first, do not reinvent std.mem, std.ascii, std.math, and the edge cases hand-rolled versions miss.
  • docs/comments.md - comment the why not the what, when a comment earns its place, and why a self-evident name needs none.
  • docs/porting.md - porting C to Zig: fix the bugs, keep the interface familiar, use Zig's features, introduce portability.
  • docs/error-handling.md - assert-vs-recover, untrusted input rules, observable recovery.
  • docs/testing.md - meaningful unit tests, comptime asserts as compile-time invariants, expected/suspicious/regression coverage.
  • docs/concurrency.md - atomics, memory ordering, volatile MMIO, multi-instance safety.

About

Zig coding philosophy for Midstall

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors