4 releases (breaking)
Uses new Rust 2024
| 0.4.0 | Dec 12, 2025 |
|---|---|
| 0.3.0 | Dec 8, 2025 |
| 0.2.0 | Dec 5, 2025 |
| 0.1.0 | Dec 3, 2025 |
#196 in Command-line interface
140KB
2K
SLoC
Musubi
A beautiful diagnostics renderer for compiler errors and warnings
Overview • Key Features • Installation • Quick Start • C API • Lua API • Testing
📦 For Rust Users: This README covers the complete Musubi project (Lua/C/Rust implementations).
Looking for Rust API documentation? → See the comprehensive Rust API docs with examples and usage guides.
Rust crate:musubi-rs
Overview
Musubi (結び, "connection" in Japanese) is a high-performance diagnostics renderer inspired by Rust's Ariadne library. It produces beautiful, color-coded diagnostic messages with precise source location highlighting, multi-line spans, and intelligent label clustering.
Originally ported from Rust's Ariadne library, Musubi has evolved into a production-ready multi-language implementation:
- C Library: High-performance core with Lua bindings (
musubi.h,musubi.c) - Rust Crate: Safe FFI wrapper with ergonomic builder API (
musubi-rs)
Both implementations produce identical output and are thoroughly tested (26 Rust unit tests + 30 doc tests, 100 Lua tests).
Key Features
✨ Beautiful Output
- Multi-line diagnostics with color-coded labels
- Intelligent label clustering and virtual row rendering
- Unicode and CJK character support
- ASCII/Unicode glyph sets for terminal compatibility
🚀 Performance Optimized
- O(n) rendering complexity (vs original O(n²))
- Pre-computed width caching for UTF-8 strings
- Binary search for line windowing calculations
- Zero-copy source file handling with streaming support
🎯 Improved Implementation
- Cleaner Implement with seprated small functions
- Bugfixes towards original Ariadne implement
- New feature: Line limited support
- New feature: No message label rendered
🛡️ Production Ready
- 100% test coverage (all reachable code covered)
- Memory-safe C implementation
- Comprehensive error handling
- Tested on Lua 5.1, 5.4, and LuaJIT
Example
local mu = require "musubi"
local cg = mu.colorgen()
print(
mu.report(12)
:code "3"
:title("Error", "Incompatible types")
:label(33, 33):message("This is of type Nat"):color(cg:next())
:label(43, 45):message("This is of type Str"):color(cg:next())
:label(12, 48):message("This values are outputs of this match expression"):color(cg:next())
:label(1, 48):message("The definition has a problem"):color(cg:next())
:label(51, 76):message("Usage of definition here"):color(cg:next())
:note "Outputs of match expressions must coerce to the same type"
:source([[
def five = match () in {
() => 5,
() => "5",
}
def six =
five
+ 1
]], "sample.tao")
:render())
Output:
Installation
Requirements
Rust Crate:
- Rust 1.56+ (edition 2024)
- No external dependencies (self-contained C implementation)
C Library with Lua Bindings:
- C89-compatible compiler (GCC, Clang, MSVC)
- Lua 5.1+ headers for Lua bindings
- Optional:
lcovfor coverage reports
Building
Rust:
cargo add musubi-rs
C Library with Lua Bindings:
# Compile shared library
gcc -O3 -Wall -shared -fPIC -o musubi.so musubi.c -llua
# Or with coverage instrumentation
gcc -shared -fPIC --coverage -o musubi.so musubi.c -llua
macOS:
gcc -O3 -Wall -shared -undefined dynamic_lookup -o musubi.so musubi.c
Quick Start
Basic Usage (C Bindings)
local mu = require "musubi"
-- Create a color generator for automatic color cycling
local cg = mu.colorgen()
-- Build a report
local report = mu.report(14) -- Primary error position
:title("Error", "Something went wrong")
:code("E001")
:label(14, 14):message("This is the problem"):color(cg:next())
:note("Try fixing this by...")
:source("local x = 10 + 'hello'", "example.js")
:render()
print(report)
Configuration
local mu = require "musubi"
local cfg = mu.config()
:compact(true) -- Enable compact mode
:cross_gap(true) -- Draw arrows across line gaps
:tab_width(4) -- Tab expansion width
:limit_width(80) -- Truncate long lines to 80 columns
:char_set "unicode" -- Use Unicode box-drawing characters
:index_type "char" -- Use character offsets (vs "byte")
:ambi_width(1) -- Ambiguous character width (1 or 2)
:column_order(false) -- Use natural label ordering (default)
:align_messages(true) -- Align label messages (default)
mu.report(0)
:config(cfg)
-- ... rest of report
Multi-Source Files
local mu = require "musubi"
mu.report(0)
:label(10, 20, 1):message("Defined here") -- src_id=1, first source
:label(50, 60, 2):message("Used here") -- src_id=2, second source
:source("fn foo() { ... }", "foo.rs")
:source("fn bar() { foo(); }", "bar.rs")
:render()
File Sources (C Bindings Only)
local mu = require "musubi"
local io = require "io"
local fp = io.open("large_file.txt", "r")
mu.report(0)
:source(fp, "large_file.txt") -- Streams file on-demand
:label(100, 150):message("Error in large file")
:render()
Notice that if you use file handle on Windows, the musubi.so must not be built as static linking (/MT).
C API Usage
musubi is an stb-style single-header library. You only need musubi.h - no separate compilation or linking required. Also see sokol for more examples of stb-style libraries.
Setup
In ONE C/C++ file (typically your main file), define MU_IMPLEMENTATION before including:
#define MU_IMPLEMENTATION
#include "musubi.h"
In all other files, just include the header normally:
#include "musubi.h" // Only declarations, no implementation
For single-file projects, use MU_STATIC_API to make all functions static:
#define MU_STATIC_API // Automatically defines MU_IMPLEMENTATION
#include "musubi.h"
Basic Example
#define MU_IMPLEMENTATION
#include <stdio.h>
#include <string.h>
#include "musubi.h"
static int stdout_writer(void *ud, const char *data, size_t len) {
fwrite(data, 1, len, stdout);
return 0; /* Success */
}
int main(void) {
mu_Report *R;
mu_Cache *C = NULL;
mu_ColorGen cg;
mu_ColorCode color1;
/* Initialize color generator */
mu_initcolorgen(&cg, 0.5f);
mu_gencolor(&cg, &color1);
/* Create Cache and add a source */
mu_addmemory(&C, mu_literal("local x = 10 + 'hello'"),
mu_literal("example.lua"));
/* Create Report and configure */
R = mu_new(NULL, NULL); /* NULL, NULL = use default malloc */
mu_title(R, MU_ERROR, mu_literal(""), mu_literal("Type mismatch"));
mu_code(R, mu_literal("E001"));
/* Add a label with message and color */
mu_label(R, 15, 22, 0);
mu_message(R, mu_literal("expected number, got string"), 0);
mu_color(R, mu_fromcolorcode, &color1);
/* Render to stdout */
mu_writer(R, stdout_writer, NULL);
mu_render(R, 14, C);
/* Cleanup */
mu_delete(R);
mu_delcache(C);
return 0;
}
Source/Cache Lifecycle
Key Concept: mu_Source IS-A mu_Cache. A single Source can be used wherever Cache is expected:
mu_Cache *C = NULL; /* Start with NULL Cache */
mu_Source *S = mu_addmemory(&C, content, name); /* Auto-upgrades C if needed */
mu_render(R, pos, (mu_Cache*)S); /* Source can be used as Cache */
mu_render(R, pos, C); /* Or use C directly, as it have been updated with same source */
Auto-Upgrade Mechanism:
- First source:
mu_addmemory(&C, ...)whereC == NULLcreates a single Source - Second source:
mu_addmemory(&C, ...)automatically upgrades to multi-Source Cache - Transparent to user: always use
mu_addsource(&C, ...)with double pointer
Lifecycle Management:
- Create Cache:
C = mu_newcache(allocf, ud)with allocator or start withC = NULL - Add sources:
mu_addmemory(&C, ...)ormu_addfile(&C, ...) - Render:
mu_render(R, pos, C)uses Cache to fetch source lines, pos is always pointed to the first source (id 0) in cache. - Cleanup:
mu_delcache(C)frees Cache and all Sources
Ownership Rules:
- Cache owns all Sources added via
mu_addmemory/mu_addfile - Report does NOT own Cache - you must call
mu_delcache(C)manually - All string slices (
mu_Slice) must outlivemu_render()call
Multi-Source Example
mu_Cache *C = NULL; /* Start with NULL */
mu_Source *S1 = mu_addmemory(&C, mu_lslice("fn foo() { }", 12),
mu_lslice("foo.c", 5));
mu_Source *S2 = mu_addmemory(&C, mu_lslice("fn bar() { foo(); }", 19),
mu_lslice("bar.c", 5));
/* Cross-file diagnostic */
mu_Report *R = mu_new(NULL, NULL);
mu_title(R, MU_ERROR, mu_literal(""), mu_literal("Undefined reference"));
mu_label(R, 11, 14, 1); /* bar.c: source id 1 */
mu_message(R, mu_literal("called here"), 0);
mu_label(R, 3, 6, 0); /* foo.c: source id 0 */
mu_message(R, mu_literal("defined here"), 0);
mu_writer(R, stdout_writer, NULL); /* See Basic Example for stdout_writer */
mu_render(R, 11, C); /* Position in foo.c (source id 0) */
mu_delete(R);
mu_delcache(C); /* Frees both S1 and S2 */
File Streaming
For large files, use mu_addfile to stream content on-demand:
mu_Cache *C = NULL;
FILE *fp = fopen("large_file.c", "r");
mu_Source *S = mu_addfile(&C, fp, mu_lslice("large_file.c", 12));
/* musubi reads lines only when needed for rendering */
mu_render(R, pos, C);
fclose(fp); /* Close after rendering */
mu_delcache(C);
Important:
- File must remain open during
mu_render()call mu_addfile(&C, NULL, path)opens file internally - musubi will close it onmu_delcache()- When passing your own
FILE*, you must close it yourself after rendering
Error Handling
All API functions return int error codes:
int err;
err = mu_label(R, 10, 20, 0);
if (err != MU_OK) {
switch (err) {
case MU_ERRPARAM: fprintf(stderr, "Invalid parameter\n"); break;
case MU_ERRSRC: fprintf(stderr, "Source not found\n"); break;
case MU_ERRFILE: fprintf(stderr, "File I/O error\n"); break;
}
mu_delete(R);
return 1;
}
err = mu_render(R, pos, C);
if (err != MU_OK) {
/* Handle error */
}
Custom Allocators
Provide custom allocator for memory control:
void* my_alloc(void *ud, void *ptr, size_t nsize, size_t osize) {
void *newptr;
if (nsize == 0) {
free(ptr);
return NULL;
}
newptr = realloc(ptr, nsize);
if (newptr == NULL) {
/* handle out-of-memory yourself, or musubi may abort */
}
return newptr;
}
void *my_userdata = /* your context */;
mu_Cache *C = mu_newcache(my_alloc, my_userdata);
mu_Report *R = mu_new(my_alloc, my_userdata);
If alloc fails (returns NULL), you must jumps out of current flow (e.g., longjmp), or musubi may abort due to out-of-memory.
Allocator signature: void* (*mu_Allocf)(void *ud, void *ptr, size_t nsize, size_t osize)
ptr == NULL: Allocatensizebytesnsize == 0: Freeptr(allocated withosizebytes)- Otherwise: Reallocate
ptrfromosizetonsizebytes
C API Reference
Types:
mu_Report- Diagnostic report buildermu_Cache- Multi-source containermu_Source- Single source (can be used as Cache)mu_Slice- String slice{const char *p, *e}mu_ColorGen- Color generator statemu_ColorCode- Pre-generated color code bufferchar[32]mu_Allocf- Allocator function typemu_Writer- Output writer function typeint (*)(void *ud, const char *data, size_t len)mu_Color- Color generator function typemu_Chunk (*)(void *ud, mu_ColorKind kind)
Cache Management:
mu_Cache* mu_newcache(mu_Allocf *allocf, void *ud)- Create empty Cachevoid mu_delcache(mu_Cache *C)- Free Cache and all Sourcesmu_Source* mu_addmemory(mu_Cache **pC, mu_Slice content, mu_Slice name)- Add in-memory sourcemu_Source* mu_addfile(mu_Cache **pC, FILE *fp, mu_Slice path)- Add file sourceunsigned mu_sourcecount(const mu_Cache *C)- Get number of sources
Report Building:
mu_Report* mu_new(mu_Allocf *allocf, void *ud)- Create new Reportvoid mu_delete(mu_Report *R)- Free Reportvoid mu_reset(mu_Report *R)- Reset Report for reuseint mu_title(mu_Report *R, mu_Level level, mu_Slice custom, mu_Slice msg)- Set kind and titleint mu_code(mu_Report *R, mu_Slice code)- Set error codeint mu_label(mu_Report *R, size_t start, size_t end, mu_Id src_id)- Add label spanint mu_message(mu_Report *R, mu_Slice msg, int width)- Set message for last labelint mu_color(mu_Report *R, mu_Color *color, void *ud)- Set color function for last labelint mu_order(mu_Report *R, int order)- Set order for last labelint mu_priority(mu_Report *R, int priority)- Set priority for last labelint mu_note(mu_Report *R, mu_Slice note)- Add footer noteint mu_help(mu_Report *R, mu_Slice help)- Add help text
Rendering:
int mu_writer(mu_Report *R, mu_Writer *fn, void *ud)- Set output writer functionint mu_render(mu_Report *R, size_t pos, const mu_Cache *C)- Render diagnostic
Configuration:
void mu_initconfig(mu_Config *cfg)- Initialize config with defaultsint mu_config(mu_Report *R, const mu_Config *cfg)- Apply configuration
Color Generation:
void mu_initcolorgen(mu_ColorGen *cg, float min_brightness)- Initialize color generatorvoid mu_gencolor(mu_ColorGen *cg, mu_ColorCode *out)- Generate next color codemu_Chunk mu_fromcolorcode(void *ud, mu_ColorKind kind)- Color function for pre-generated codesmu_Chunk mu_default_color(void *ud, mu_ColorKind kind)- Default color scheme
Utilities:
mu_Slice mu_lslice(const char *s, size_t len)- Create slice with explicit lengthmu_literal("text")- Macro: create slice from string literal (compile-time length)mu_slice(str)- Macro: create slice from C string (usesstrlen)
Constants:
- Error codes:
MU_OK(0),MU_ERRPARAM(-1),MU_ERRSRC(-2),MU_ERRFILE(-3) - Levels:
MU_ERROR,MU_WARNING,MU_CUSTOM_LEVEL
For complete API documentation, see musubi.h header file and .github/c_port.md.
Lua API Reference
Report Builder
| Method | Description |
|---|---|
mu.report(pos, src_id?) |
Create a new report at position pos |
:title(level, message) |
Set report level ("Error", "Warning") and title |
:code(code) |
Set optional error code (e.g., "E0308") |
:label(start, end?, src_id?) |
Add a label span (half-open interval [start, end)) |
:message(text, width?) |
Attach message to the last added label |
:color(color) |
Set color for the last added label |
:order(n) |
Set display order for the last label |
:priority(n) |
Set priority for clustering |
:note(text) |
Add a note to the footer |
:help(text) |
Add a help message to the footer |
:source(content, name?, offset?) |
Register a source (string or FILE*) with line offset (0 default) |
:render(writer?) |
Render the report (returns string or calls writer function) |
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
compact |
boolean | false |
Compact mode (works with underlines) |
cross_gap |
boolean | true |
Draw arrows across skipped lines |
underlines |
boolean | true |
Draw underlines for single-line labels |
column_order |
boolean | false |
Simple column order (true) vs natural ordering (false) |
align_messages |
boolean | true |
Align label messages to same column |
multiline_arrows |
boolean | true |
Use arrows for multi-line spans |
tab_width |
integer | 4 |
Number of spaces per tab |
limit_width |
integer | 0 |
Max line width (0 = unlimited) |
ambi_width |
integer | 1 |
Width of ambiguous Unicode characters |
label_attach |
string | "middle" |
Label attachment point ("start", "middle", "end") |
index_type |
string | "char" |
Position indexing ("char" or "byte") |
char_set |
string | "unicode" |
Glyph set ("unicode" or "ascii") |
color |
boolean | true |
Enable ANSI color codes |
Cache API
Multi-source diagnostics: Use mu.cache() to manage multiple source files:
local cache = mu.cache()
:source("local x = 1 + '2'", "main.lua")
:file("lib.lua") -- Loads from file system
local report = mu.report(15, 0) -- Position in source 0 (main.lua)
:label(15, 18):message("error here")
cache:render(report)
Length operator: #cache returns the number of sources.
For detailed Lua API documentation with examples, see musubi.def.lua.
Color Generator
local cg = mu.colorgen(min_brightness?) -- min_brightness ∈ [0, 1], default 0.5
local color_func = cg:next() -- Get next color in cycle
Architecture
Rendering Pipeline
Report:render()
├─ Context Creation (group labels by source, calculate widths)
├─ Header Rendering (error level, code, message)
├─ For each source group:
│ ├─ Reference Header (file:line:col)
│ ├─ Line Rendering:
│ │ ├─ Label Clustering (group overlapping labels)
│ │ ├─ Window Calculation (when limit_width > 0)
│ │ ├─ Virtual Row Splitting (multi-line labels)
│ │ └─ For each cluster:
│ │ ├─ Line Content (with label highlighting)
│ │ └─ Arrow Drawing (underlines, connectors, messages)
│ └─ Empty Line
└─ Footer Rendering (notes, help messages)
Key Design Decisions
Intervals:
- All position named
start/enduse half-open intervals[start, end) - All position named
first/lastuse close intervals[fist, last]
Width Caching:
- Pre-compute cumulative display widths for each line
- Binary search (
muC_widthindex) for O(log n) position lookups - Handles UTF-8 multi-byte characters, Emoji, RI, CJK double-width, tabs
Label Clustering:
- Group overlapping/nearby labels into virtual rows
- Separate inline labels (single line) from multiline labels
- Dynamic column range calculation for windowing
Memory Management (C):
- Caller provides allocator function (defaults to
malloc/free) - Dynamic arrays with geometric growth (muA_* macros)
- External pointers (messages, source names) must outlive render call
Testing
Running Tests
# Compile with coverage
gcc -ggdb -shared --coverage -o musubi.so musubi.c
# Run tests (uses C bindings by default)
lua test.lua
# Generate coverage report
lcov -d . -c -o lcov.info
genhtml lcov.info -o coverage/
Test Coverage
Both implementations maintain 100% test coverage:
- 100 test cases covering all rendering paths
- Edge cases: zero-width spans, CJK characters, tab expansion, window truncation
- Regression tests for all fixed bugs
- Pixel-perfect output verification (2400+ lines of expected output)
Test Categories:
- Basic rendering (labels, messages, colors)
- Multi-line spans and clustering
- Line width limiting and windowing
- Unicode and CJK character handling
- Configuration options (compact, cross_gap, etc.)
- Multi-source file support
- File streaming (C only)
Implementation Notes
Differences from Rust Ariadne
Improvements:
- Cleaner margin render handling
- Explicit virtual row rendering for multi-line labels
- Width-based windowing with binary search optimization
- Label without message supports
Limitations:
- Only supports
\nnewlines (not Unicode line separators) - Not full UAX#29 grapheme cluster breaking (only support ZWJ & RI now)
C Implementation Details
See .github/c_port.md for detailed implementation notes:
- API constraints and call ordering requirements
- Memory management and lifetime rules
- UTF-8 handling and Unicode width calculations
- Source lifecycle and file streaming
- Known limitations and edge cases
Project Structure
See .github/project-structure.md for:
- Detailed architecture documentation
- Data structure definitions
- Rendering algorithm explanations
- Bug fix history and rationale
Contributing
Contributions are welcome! Please:
- Run tests before submitting:
lua test.lua - Maintain 100% coverage: Add tests for new features
- Follow existing style: Lua uses tabs, C uses 4 spaces
- Update documentation: Keep README and .github/*.md in sync
Development Workflow
# Run tests with coverage
lua -lluacov test.lua
luacov ariadne.lua
# Find uncovered lines
grep '^\*\+0 ' luacov.report.out
# Run specific test
lua test.lua TestBasic.test_simple_label
License
MIT License - See LICENSE for details.
Credits
- Original Ariadne library: zesterer/ariadne
- UTF-8 support: starwing/luautf8
- Test framework: LuaUnit
Related Projects
- Ariadne (Rust) - Original implementation
- Annotate Snippets (Rust) - Similar project
- Miette (Rust) - Fancy diagnostics library
- Codespan (Rust) - Alternative approach
Made with ❤️ for better compiler diagnostics
No runtime deps
~235KB