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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ jobs:
- name: Verify shared-backing evidence
run: python scripts/verify-allocation-evidence.py target/allocation-evidence.log

layout-32-bit:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Set up stable Rust with 32-bit target
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
with:
toolchain: stable
targets: i686-pc-windows-msvc

- name: Verify 32-bit compact layout
run: cargo test --target i686-pc-windows-msvc --test layout_snapshot --all-features -- --nocapture

dependency-audit:
runs-on: ubuntu-latest
steps:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
- Exposed the reverse, clone, and fused iterator guarantees of `lines()`.
- Added fail-closed allocation evidence for exact/spare freezes, shared input,
cloning, characters, and short concatenation.
- Reduced `CheetahString` and `Option<CheetahString>` from 32 to 24 bytes on
64-bit targets while preserving the 23-byte inline capacity. The stable
representation uses constrained Rust enum discriminants as layout niches and
does not encode pointers as integers.
Comment on lines +35 to +38

### Migration

Expand Down
55 changes: 55 additions & 0 deletions LAYOUT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Stable layout contract

`CheetahString` keeps three storage modes in a private Rust enum:

| Mode | Payload | Ownership |
|---|---|---|
| Inline | constrained length plus 23 UTF-8 bytes | stored in the value |
| Static | `&'static str` | borrowed forever |
| Shared | `Arc<str>` | immutable reference-counted ownership |

The inline length is a `repr(u8)` enum with exactly 24 valid values, from 0
through 23. Its other bit patterns are invalid discriminants. Rust can use those
invalid patterns as niches for the outer storage variants and for `Option`, so
the largest 24-byte payload does not require a separate discriminant byte.

This design preserves pointer provenance. It does not cast pointers to integers,
reconstruct pointers, use a union, implement manual drop logic, or add an unsafe
block. Static and shared values remain ordinary references and `Arc<str>` values.

## Enforced sizes

The supported 32-bit and 64-bit targets enforce:

| Type | Size |
|---|---:|
| `CheetahString` | 24 bytes |
| `Option<CheetahString>` | 24 bytes |
| 10,000 `CheetahString` vector slots | 240,000 bytes |
| 10,000 `(CheetahString, u64)` map-entry payloads on 64-bit | 320,000 bytes |

The previous 64-bit representation used 320,000 bytes for 10,000 vector slots;
the compact representation saves 80,000 bytes, or 25%, before allocator
overhead. Its vector slot size now matches `String`, while long clones retain
the O(1), zero-allocation `Arc<str>` behavior that `String` does not provide.

Rust enum layout is not a public ABI guarantee. The project therefore treats
24 bytes as a tested performance contract rather than an FFI promise. The CI
matrix checks stable and nightly toolchains, the Rust 1.95 packaged consumer,
and a 32-bit target. Any compiler that stops applying the required niche
optimization fails the layout gate instead of silently changing the footprint.

## Verification

```bash
cargo test --test layout_snapshot --all-features -- --nocapture
cargo test --test allocation_contract --all-features -- --test-threads=1
cargo bench --bench shared_backing -- __allocation_evidence_only__ --noplot \
2>&1 | tee target/allocation-evidence.log
python scripts/verify-allocation-evidence.py target/allocation-evidence.log
```

Miri remains the behavioral provenance check for the stable representation.
The layout snapshot and schema-v3 benchmark evidence independently enforce the
object size, container slot footprint, allocation counts, and shared-pointer
retention contract.
10 changes: 7 additions & 3 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ noise cannot establish a portable latency threshold.
| Long borrowed construction | 1 | One live `Arc<str>` backing |
| Exact-capacity owned/builder freeze | 1 | One live `Arc<str>` backing |
| Spare-capacity owned/builder freeze | 2 | Shrink/reallocation plus `Arc<str>` backing |
| `CheetahString` object size | N/A | 32 bytes on supported 64-bit targets |
| `CheetahString` object size | N/A | 24 bytes on supported 32-bit and 64-bit targets |
| `Option<CheetahString>` object size | N/A | 24 bytes on supported 32-bit and 64-bit targets |
| 10,000 vector element slots | N/A | 240,000 bytes; 80,000 below the prior contract |
| 10,000 `(CheetahString, u64)` payloads | N/A | 320,000 bytes; equal to `(String, u64)` |

The allocation count includes allocation and reallocation events during the
measured conversion. It is intentionally different from the number of live
Expand All @@ -41,9 +44,10 @@ cargo bench --bench shared_backing -- __allocation_evidence_only__ --noplot \
python scripts/verify-allocation-evidence.py target/allocation-evidence.log
```

The verifier requires one schema-v2 `SHARED_BACKING_EVIDENCE` record and fails
The verifier requires one schema-v3 `SHARED_BACKING_EVIDENCE` record and fails
closed when a required field is absent, an allocation count regresses, the
64-bit layout changes, or long `Arc<str>` input does not retain its pointer.
64-bit layout or downstream slot footprint changes, or long `Arc<str>` input
does not retain its pointer.

## Timing policy

Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ architecture.
| Long borrowed text or exact-capacity `String` | Shared | 1 | 0 |
| Long spare-capacity `String` / builder | Shared | 2: shrink/reallocate, then Arc backing | 0 |

On supported 32-bit and 64-bit targets, both `CheetahString` and
`Option<CheetahString>` occupy 24 bytes. A 10,000-element vector therefore uses
240,000 bytes of element slots instead of the previous 320,000-byte contract.
This is achieved with safe Rust enum niches; string pointers are never converted
to integers. See [Stable layout](LAYOUT.md) for the exact representation and
portability gate.

The representation has no mutable `Owned(String)` state. Construction history
therefore cannot change clone complexity. Use:

Expand Down Expand Up @@ -192,7 +199,8 @@ Hosted-runner and local benchmark results are diagnostic; they do not
independently establish a release-grade performance pass. The versioned
allocation and layout tests are the deterministic performance contracts.
See [Performance contracts](PERFORMANCE.md) for the exact enforced budgets and
the distinction between deterministic gates and diagnostic timing results.
the distinction between deterministic gates and diagnostic timing results, and
[Stable layout](LAYOUT.md) for the provenance-preserving 24-byte representation.

## Safety and portability

Expand Down
10 changes: 9 additions & 1 deletion SAFETY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ part of this model and is not exported by the crate.
## Core invariants

- Every `CheetahString` is valid UTF-8 for its complete lifetime.
- Inline length never exceeds the 23-byte inline buffer.
- Inline length is represented by a private 0-through-23 enum and never exceeds
the 23-byte inline buffer. Its invalid discriminants are compiler layout
niches; they are never constructed as inline lengths.
- Static storage contains a valid `&'static str`.
- Shared storage is an owned `Arc<str>` and preserves pointer provenance.
- `CheetahBytes` has byte semantics and does not imply UTF-8.
Expand All @@ -28,6 +30,12 @@ Safe constructors perform UTF-8 validation before entering those helpers;
public unchecked constructors forward their documented caller contract in an
explicit unsafe block.

The 24-byte representation does not add an unsafe boundary. `Static` remains a
normal `&'static str`, `Shared` remains a normal `Arc<str>`, and the outer Rust
enum uses the constrained inline-length discriminants as niches. No pointer is
converted to an integer or reconstructed. Layout snapshots gate the compiler
optimization separately from Miri's behavioral provenance checks.

## Verification

Run the stable representation under Miri:
Expand Down
11 changes: 10 additions & 1 deletion benches/shared_backing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ fn emit_allocation_evidence() {
let arc_string_rss_after = current_rss_bytes();

let evidence = json!({
"schema_version": 2,
"schema_version": 3,
"object_sizes": {
"Inline|Arc<str>": size_of::<ArcStrCandidate>(),
"Inline|Arc<String>": size_of::<ArcStringCandidate>(),
Expand Down Expand Up @@ -311,6 +311,15 @@ fn emit_allocation_evidence() {
"invariants": {
"from_arc_str_pointer_reused": cheetah_arc.as_bytes().as_ptr() == shared_source_pointer
},
"downstream_slots": {
"items": 10_000,
"vector_bytes": size_of::<CheetahString>() * 10_000,
"string_vector_bytes": size_of::<String>() * 10_000,
"map_entry_payload_bytes": size_of::<(CheetahString, u64)>() * 10_000,
"string_map_entry_payload_bytes": size_of::<(String, u64)>() * 10_000,
"previous_32_byte_vector_bytes": 32 * 10_000,
"vector_bytes_saved_vs_previous": (32 - size_of::<CheetahString>()) * 10_000
},
"rss": {
"Arc<str>": {
"before_bytes": arc_str_rss_before,
Expand Down
7 changes: 5 additions & 2 deletions scripts/bench-all.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ $benchmarkIds = @(
[ordered]@{
schema_version = 1
capture_schema_version = "cheetah-string-capture-v3"
benchmark_schema_version = "cheetah-string-bench-v2"
benchmark_schema_version = "cheetah-string-bench-v3"
criterion_schema_version = "criterion-0.5"
crate = "cheetah-string"
git_sha = $gitSha
Expand Down Expand Up @@ -154,7 +154,7 @@ Invoke-CargoCapture "allocation-contract.txt" @(
)
Assert-TestExecuted "allocation-contract.txt"
[ordered]@{
schema_version = 2
schema_version = 3
layout_contract = "passed"
allocation_contract = "passed"
clone_allocations_max = 0
Expand All @@ -163,6 +163,9 @@ Assert-TestExecuted "allocation-contract.txt"
inline_concat_allocations_max = 0
owned_exact_freeze_allocations_max = 1
owned_spare_freeze_allocations_max = 2
cheetah_string_size_64 = 24
option_cheetah_string_size_64 = 24
vector_slots_per_10000_bytes = 240000
source = "tests/allocation_contract.rs"
} | ConvertTo-Json -Depth 4 | Set-Content -Encoding utf8 -LiteralPath (Join-Path $ResultDir "contracts.json")
Invoke-CargoCapture "layout-bench.txt" (@(
Expand Down
7 changes: 5 additions & 2 deletions scripts/bench-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ json_escape() {
printf '{\n'
printf ' "schema_version": 1,\n'
printf ' "capture_schema_version": "%s",\n' "$CAPTURE_SCHEMA"
printf ' "benchmark_schema_version": "cheetah-string-bench-v2",\n'
printf ' "benchmark_schema_version": "cheetah-string-bench-v3",\n'
printf ' "criterion_schema_version": "criterion-0.5",\n'
printf ' "crate": "cheetah-string",\n'
printf ' "git_sha": "%s",\n' "$(json_escape "$GIT_SHA")"
Expand Down Expand Up @@ -153,7 +153,7 @@ run_cargo allocation-contract.txt test --test allocation_contract --all-features
require_test_passed allocation-contract.txt
cat > "$RESULT_DIR/contracts.json" <<'JSON'
{
"schema_version": 2,
"schema_version": 3,
"layout_contract": "passed",
"allocation_contract": "passed",
"clone_allocations_max": 0,
Expand All @@ -162,6 +162,9 @@ cat > "$RESULT_DIR/contracts.json" <<'JSON'
"inline_concat_allocations_max": 0,
"owned_exact_freeze_allocations_max": 1,
"owned_spare_freeze_allocations_max": 2,
"cheetah_string_size_64": 24,
"option_cheetah_string_size_64": 24,
"vector_slots_per_10000_bytes": 240000,
"source": "tests/allocation_contract.rs"
}
JSON
Expand Down
20 changes: 18 additions & 2 deletions scripts/tests/test_allocation_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

def valid_evidence() -> dict:
return {
"schema_version": 2,
"object_sizes": {"CheetahString": 32},
"schema_version": 3,
"object_sizes": {"CheetahString": 24},
"allocations": {
"CheetahString": {
"borrowed": {"count": 1, "bytes": 1040},
Expand All @@ -31,6 +31,15 @@ def valid_evidence() -> dict:
}
},
"invariants": {"from_arc_str_pointer_reused": True},
"downstream_slots": {
"items": 10_000,
"vector_bytes": 240_000,
"string_vector_bytes": 240_000,
"map_entry_payload_bytes": 320_000,
"string_map_entry_payload_bytes": 320_000,
"previous_32_byte_vector_bytes": 320_000,
"vector_bytes_saved_vs_previous": 80_000,
},
}


Expand All @@ -56,6 +65,13 @@ def test_missing_or_false_pointer_invariant_is_rejected(self) -> None:
with self.assertRaises(VERIFIER.EvidenceError):
VERIFIER.validate_evidence(evidence)

def test_downstream_slot_regression_is_rejected(self) -> None:
evidence = valid_evidence()
evidence["downstream_slots"]["vector_bytes"] = 320_000

with self.assertRaisesRegex(VERIFIER.EvidenceError, "vector_bytes"):
VERIFIER.validate_evidence(evidence)

def test_log_parser_requires_one_evidence_record(self) -> None:
record = json.dumps(valid_evidence(), separators=(",", ":"))
parsed = VERIFIER.parse_log(f"noise\nSHARED_BACKING_EVIDENCE={record}\n")
Expand Down
15 changes: 15 additions & 0 deletions scripts/tests/test_repository_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

ROOT = Path(__file__).resolve().parents[2]
WORKFLOWS = ROOT / ".github" / "workflows"


def read(path: str) -> str:
return (ROOT / path).read_text(encoding="utf-8")
Comment on lines 8 to 13

Expand Down Expand Up @@ -48,6 +50,18 @@ def test_internal_unchecked_helpers_are_unsafe_boundaries(self) -> None:
):
self.assertRegex(source, rf"unsafe fn {helper}\b")

def test_compact_layout_does_not_integerize_or_reconstruct_pointers(self) -> None:
source = read("src/inline.rs")
self.assertIn("enum InlineLength", source)
self.assertEqual(source.count("unsafe {"), 2)
for forbidden in (
"from_raw_parts",
"transmute",
"expose_provenance",
"with_exposed_provenance",
):
self.assertNotIn(forbidden, source)
Comment on lines +53 to +63

def test_workflow_actions_are_immutable(self) -> None:
for workflow in sorted(WORKFLOWS.glob("*.y*ml")):
text = workflow.read_text(encoding="utf-8")
Expand All @@ -66,6 +80,7 @@ def test_ci_contains_reproducible_engineering_gates(self) -> None:
"cargo audit -D warnings",
"scripts/check-msrv-package.sh 1.95",
"cargo test --test allocation_contract --all-features -- --test-threads=1",
"cargo test --target i686-pc-windows-msvc --test layout_snapshot --all-features -- --nocapture",
"python scripts/verify-allocation-evidence.py",
):
self.assertIn(command, ci)
Expand Down
22 changes: 18 additions & 4 deletions scripts/verify-allocation-evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ def require_allocation(


def validate_evidence(evidence: dict[str, Any]) -> None:
if evidence.get("schema_version") != 2:
raise EvidenceError("schema_version must be 2")
if evidence.get("schema_version") != 3:
raise EvidenceError("schema_version must be 3")

sizes = require_mapping(evidence.get("object_sizes"), "object_sizes")
if sizes.get("CheetahString") != 32:
raise EvidenceError("object_sizes.CheetahString must remain 32 on the 64-bit gate runner")
if sizes.get("CheetahString") != 24:
raise EvidenceError("object_sizes.CheetahString must remain 24 on the 64-bit gate runner")

allocation_groups = require_mapping(evidence.get("allocations"), "allocations")
allocations = require_mapping(allocation_groups.get("CheetahString"), "allocations.CheetahString")
Expand All @@ -78,6 +78,20 @@ def validate_evidence(evidence: dict[str, Any]) -> None:
if invariants.get("from_arc_str_pointer_reused") is not True:
raise EvidenceError("invariants.from_arc_str_pointer_reused must be true")

slots = require_mapping(evidence.get("downstream_slots"), "downstream_slots")
expected_slots = {
"items": 10_000,
"vector_bytes": 240_000,
"string_vector_bytes": 240_000,
"map_entry_payload_bytes": 320_000,
"string_map_entry_payload_bytes": 320_000,
"previous_32_byte_vector_bytes": 320_000,
"vector_bytes_saved_vs_previous": 80_000,
}
for name, expected in expected_slots.items():
if slots.get(name) != expected:
raise EvidenceError(f"downstream_slots.{name} must be {expected}")


def main() -> int:
parser = argparse.ArgumentParser()
Expand Down
Loading
Loading