Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .cursor/rules/rust-standards.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ alwaysApply: false
Run `./scripts/check_doc_76.py` to verify compliance.


## DERIVE_LAYOUT (Derive Layout & Formatting)

Multi-trait `#[derive(...)]` macros must be split into separate,
single-trait lines, ordered alphabetically by trait name. Tightly coupled
traits may remain on one line: `#[derive(Eq, PartialEq)]`,
`#[derive(Ord, PartialOrd)]`.

`rustfmt.toml` sets `merge_derives = false` to preserve this layout.

Run `./scripts/check_derives.py` to verify compliance.


## RUST_TEST_NAMING

All test methods must use `SHOUTING_SNAKE_CASE` - e.g. `TEST_PARSING()` -
Expand Down
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Diagnosticism.Rust - CHANGES <!-- omit in toc -->


## 0.3.2 - 28th June 2026

* optimisation of `nanoseconds_to_string()` — uses a custom return type `NanosecondsStr` for highly efficient conversion in vast majority of cases;


## 0.3.1 - 28th June 2026

* internal implementation improvements;
Expand Down
9 changes: 8 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ name = "diagnosticism"
readme = "README.md"
repository = "https://github.com/synesissoftware/Diagnosticism.Rust"
rust-version = "1.74"
version = "0.3.1"
version = "0.3.2"


# ##########################################################
Expand Down Expand Up @@ -95,6 +95,8 @@ test-doomgram = [

[dependencies]

base-traits = { version = "0", default-features = false, features = [
]}
rand = { version = "0.10", optional = true }

[dev-dependencies]
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,17 @@ The following optional features are defined in **Cargo.toml**:
The following function is re-exported at the crate root (and defined in the [`diagnostics`](https://docs.rs/diagnosticism/latest/diagnosticism/diagnostics/index.html) module):

* `doom_scope()` - executes a closure, records its elapsed time in a [`DoomGram`](https://docs.rs/diagnosticism/latest/diagnosticism/struct.DoomGram.html), and returns the closure's result together with the measured elapsed time (in nanoseconds). See the example [**examples/doomgram.md**](./examples/doomgram.md);
* `nanoseconds_to_string()` - formats a nanosecond count as a compact human-readable duration string (units `ns`, `µs`, `ms`, `s` with roughly three significant digits); behaviour matches [**Diagnosticism.Python**](https://github.com/synesissoftware/Diagnosticism.Python) 0.16.0;
* `nanoseconds_to_string()` - formats a nanosecond count as a compact human-readable duration string (units `ns`, `µs`, `ms`, `s` with roughly three significant digits); returns a [`NanosecondsStr`](https://docs.rs/diagnosticism/latest/diagnosticism/struct.NanosecondsStr.html); behaviour matches [**Diagnosticism.Python**](https://github.com/synesissoftware/Diagnosticism.Python) 0.16.0;

For example:

```Rust
use diagnosticism::nanoseconds_to_string;

assert_eq!( "123.4ms", nanoseconds_to_string(123_456_789, ""));
assert_eq!( "6.789µs", nanoseconds_to_string( 6_789, ""));
assert_eq!("+999.7ms", nanoseconds_to_string(999_772_000, "+"));
```


### Macros
Expand All @@ -99,6 +109,7 @@ The following structures are re-exported at the crate root (and defined in the [

* `DebugSqueezer` - used to assist with restricting the length of `Debug` forms of fields within a given width. See the example [**examples/debug_squeezer.md**](./examples/debug_squeezer.md);
* `DoomGram` - a **D**ecimal **O**rder-**O**f-**M**agnitude histo**G**ram structure that records efficiently duration values in the orders of magnitude 1ns+, 10ns+, 100ns+, 1µs+, ..., 10s+, 100s+ and provides a mechanism for displaying this histogram in a simple single 12-character display, which is useful for logging cumulative execution costs of components in long-running performance-sensitive applications. See the example [**examples/doomgram.md**](./examples/doomgram.md);
* `NanosecondsStr` - compact storage for a formatted duration string; returned by `nanoseconds_to_string()`; typical outputs fit in 15 inline UTF-8 bytes without heap allocation; implements `Display`, `Deref` to `str`, and `AsRef<str>`;
* `Ellipsis` - provides the string `"..."` to be used for fields whose `Debug` forms are not to be expressed in terse (non-`#alternate()`) output. See the example [**examples/ellipsis.md**](./examples/ellipsis.md);
* `Password` - provides strings such as `"********"` to be used for fields that are sensitive and whose `Debug` forms are not to be expressed. See the example [**examples/password.md**](./examples/password.md);

Expand Down
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

* [ ] `DebugSqueezer`: avoid formatting the entire [`Debug`](https://doc.rust-lang.org/std/fmt/trait.Debug.html) string before truncation (current implementation uses `format!()` then `truncate()`; investigate streaming/early-limit approaches that preserve readable elision suffixes without allocating for oversized output);
* [ ] [`DoomGram::to_strip()`](https://docs.rs/diagnosticism/latest/diagnosticism/struct.DoomGram.html#method.to_strip): reduce cost of building the 12-character histogram strip (currently ~20µs debug / ~2µs release; see in-code TODO in `doomgram.rs` — pre-fill with `_`, skip `gram_doom_to_char()` for zero counts, iterate bucket counts from an array rather than separate fields; validate with `benches/doomgram.rs`);
* [ ] Determine whether use of `ilog10()` can help to reduce performance costs of `DoomGram`;


<!-- ########################### end of file ########################### -->
Expand Down
129 changes: 129 additions & 0 deletions scripts/check_derives.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#! /usr/bin/env python3
"""
Verify DERIVE_LAYOUT: multi-trait `#[derive(...)]` macros must be split
into separate single-trait lines, ordered alphabetically by trait name,
except tightly coupled groups (Eq/PartialEq, Ord/PartialOrd).
"""

from __future__ import annotations

import re
import sys
from pathlib import Path


COUPLED_TRAIT_GROUPS = [
["Eq", "PartialEq"],
["Ord", "PartialOrd"],
]

PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅
FAIL = "\N{CROSS MARK}" # ❌


def lint_file(filepath: Path) -> list[str]:
errors: list[str] = []

lines = filepath.read_text(encoding="utf-8").splitlines()
i = 0

while i < len(lines):
line = lines[i]

if not re.match(r"^\s*#\[derive\(", line):
i += 1
continue

derive_block: list[tuple[int, str]] = []
start_line_num = i + 1

while i < len(lines) and re.match(r"^\s*#\[derive\(", lines[i]):
derive_block.append((i + 1, lines[i]))
i += 1

parsed_lines: list[tuple[int, str, str]] = []
block_has_error = False

for line_num, line_str in derive_block:
match = re.search(r"#\[derive\((.*?)\)\]", line_str)

if not match:
continue

traits = [
t.strip()
for t in match.group(1).split(",")
if t.strip()
]

if len(traits) > 1:
if traits not in COUPLED_TRAIT_GROUPS:
block_has_error = True
allowed = ", ".join(
f"'{', '.join(group)}'"
for group in COUPLED_TRAIT_GROUPS
)
errors.append(
f"{filepath}:{line_num}: multi-trait derive "
f"'{line_str.strip()}' is not allowed "
f"(except coupled groups: {allowed})",
)
elif len(traits) == 0:
block_has_error = True
errors.append(
f"{filepath}:{line_num}: empty derive attribute "
f"'{line_str.strip()}'",
)

sort_key = traits[0] if traits else ""
parsed_lines.append((line_num, line_str, sort_key))

if not block_has_error and len(parsed_lines) > 1:
sort_keys = [item[2] for item in parsed_lines]

if sort_keys != sorted(sort_keys):
actual = [item[1].strip() for item in parsed_lines]
expected = [
item[1].strip()
for item in sorted(parsed_lines, key=lambda x: x[2])
]
errors.append(
f"{filepath}:{start_line_num}: derive attributes not "
f"sorted alphabetically\n"
f" actual: {actual}\n"
f" expected: {expected}",
)

return errors


def main() -> int:
root = Path(__file__).resolve().parents[1]
errors: list[str] = []

for directory in ("src", "examples", "benches"):
base = root / directory

if not base.is_dir():
continue

for path in sorted(base.rglob("*.rs")):
if "target" in path.parts:
continue

errors.extend(lint_file(path))

if errors:
print(
f"{FAIL} DERIVE_LAYOUT violations:",
file=sys.stderr,
)
print("\n".join(f" {FAIL} {error}" for error in errors), file=sys.stderr)
return 1

print(f"{PASS} DERIVE_LAYOUT: ok")
return 0


if __name__ == "__main__":
raise SystemExit(main())
6 changes: 5 additions & 1 deletion src/diagnostics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ declare_and_publish!(doomgram, DoomGram, doom_scope);
declare_and_publish!(ellipsis, Ellipsis);
mod flf;
declare_and_publish!(password, Password);
declare_and_publish!(time_format, nanoseconds_to_string);
declare_and_publish!(pub
time_format,
NanosecondsStr,
nanoseconds_to_string,
);


// ///////////////////////////// end of file //////////////////////////// //
Loading
Loading