# Changelog

All notable changes to **sasso** are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Conformance is tracked separately as a ratchet against the official
[sass-spec](https://github.com/sass/sass-spec) suite — see the
[Conformance](README.md#conformance) section for the current pass rate.

## [Unreleased]

## [0.8.0] - 2026-07-06

_Crate release `v0.8.0`; ships on npm as `sasso@0.11.0` (wasm + native — same
core). Byte-identity, complete: **all 20 projects in the real-world corpus
compile byte-identical to dart-sass 1.101**
([`bench/real-world/real_world.md`](bench/real-world/real_world.md), 2.9–51×
faster end-to-end). The corpus doubled this release (tabler, AdminLTE,
reveal.js, Font Awesome, video.js, forem, nextcloud server,
jekyll-theme-chirpy, grafana, wagtail) and every divergence it surfaced was
fixed against dart-sass semantics — each pinned by a parity test._

### Fixed (`@extend` engine — dart's per-registration model)

- **`@each` over a single value — `null` included — iterates once** (dart
  `Value.asList`). Bootstrap's `valid-radius(null)` relies on it; previously
  a css-less `border-radius` map key ERRORED the whole compile (tabler).
- **The application fold runs each registration exactly once — no fixpoint.**
  Extend cycles resolve the way dart's do: `_extensionsByExtender[target]` is
  a LIVE list, so an extender that itself contains the target self-derives
  within its own registration. The old fixpoint chained one extender of an
  `@extend` through its sibling of the same rule, emitting selectors dart
  never produces.
- **Original selectors are identity-tracked** (dart `_originals` is a
  `Set.identity()`): an extension product value-equal to one of the rule's
  own selectors is coverage-trimmed in the original's favor, keeping the
  original's position and source line break (`.btn-sm,\n.btn-group-sm > .btn`;
  `h1, .h1` heading runs). dart's bare fast path (a single-simple compound
  replaced wholesale returns the extender object) carries identity too,
  scope-gated per module store — dart#1297 keeps the in-store `:is()` rewrite
  alongside its source while the cross-module variant replaces it.
- **Foreign extensions apply in dart's cross-module store-merge order**
  (downstream stores merge transitively; sibling stores reverse-first-load;
  derived entries follow their trigger's absorption order) — bulma's `%block`
  extender interleaving.
- **Identical `(target, extender)` registrations merge per module store, not
  globally**, so each keeps its own store-merge rank (chirpy's
  `#access-lastmod a:hover` position and line break).
- **One `@extend` hitting several compounds of the same complex uses dart's
  `paths` order** (first component varies fastest) on the incremental path
  too (forem's `.crayons-btn + .crayons-btn` file-selector products).
- **Pre-rule extensions apply one-shot per dart's `addSelector` timing**, and
  the one-shot gate compares registration indices, not counts.
- **A placeholder-only module whose rules were all dropped counts as empty**,
  so its group-separator blank collapses (uswds `placeholders/`).

### Fixed (module system & output fidelity)

- **The pre-module comment engine matches dart on three fronts** (uswds):
  registration deep-scans pending comments through invisible module-scope
  placeholders; re-emitted clones are fenced and never re-register onto new
  module keys; a css-less module built while the shared comment map is
  non-empty absorbs pending registrations (dart's `transitivelyContainsCss`
  includes `preModuleComments.isNotEmpty`).
- **Re-emitted pre-module clones stay out of the loader's import-run sweep**
  (nextcloud's SPDX header no longer jumps to the top of the document).
- **CSS `@import` hoisting keeps the css flow's eval-time grouping** — dart's
  blank lines come solely from group-end flags, never source gaps; only the
  seam the pulled import run vacated is re-derived (forem).
- **`meta.load-css` copies re-acquire per-rule group separators** — dart
  re-visits the combined css node-by-node (reveal.js print styles).
- **A trailing invisible chain owns the enclosing group's end at any nesting
  depth**, packing the next group tight (chirpy).
- **`@at-root` separators follow dart's `_styleRule == null` group-end gate**
  — rules flattened out of a wrapper rule pack tight (wagtail's sidebar).
- **Selector line-break parity with dart** (pseudo-arg newlines,
  parent-resolution flags), **loud comment re-indentation** per dart's
  `_loudComment`, **invisible-last-child groups pack the next group tight**,
  and **an empty module scope no longer anchors a group separator**
  (bootstrap, quasar, mastodon, govuk-frontend byte-identity).

### Added

- **`bench/real-world/`: the vetted corpus doubled to 20 projects** — every
  one compiled standalone with both engines and byte-compared on every run
  (`node bench/real-world/run.mjs check` is the regression gate).

## [0.7.0] - 2026-07-04

_Crate release `v0.7.0`; ships on npm as `sasso@0.10.0` (wasm + native — same
core). The dart-sass byte-parity campaign: every project in the new
real-world corpus ([`bench/real-world/real_world.md`](bench/real-world/real_world.md))
now compiles, and most match dart-sass 1.101 byte-for-byte._

### Added (npm package — `sasso/native`; released as `sasso@0.9.0` on npm)

- **`sasso/native` subpath: the native addon as a first-class npm entry.**
  Prebuilt binaries publish as exact-version-pinned `optionalDependencies`
  (`sasso-native-{darwin-arm64, darwin-x64, linux-x64-gnu, linux-arm64-gnu}`) —
  npm installs only the matching platform, the release workflow builds and
  byte-parity-tests every binary against the wasm reference before publishing,
  and unsupported platforms get a clear error pointing back at the wasm
  entries. Resolution order: `SASSO_NATIVE_BINARY` override → platform
  package → repo-local build.

### Added (repo — native Node addon)

- **`napi/`: a native Node addon binding the core crate directly** (F4 of
  `docs/ASYNC_PERF_ARCHITECTURE.md`) — no wasm, no asyncify. Same dart-sass
  modern API as the `sasso` npm package, verified **byte-identical** to the
  wasm engine's output across the in-repo corpora (which is itself dart-sass
  byte-exact). Async compiles each run on their own OS thread: ~3× engine
  speed over the wasm modules, and a concurrent 8-entry cold build finishes
  in ~1.14× a single compile's time (true multi-core parallelism; the wasm
  engine's single JS thread serializes CPU-bound fan-out). User importers, custom functions,
  and loggers bridge to JS; `loadPaths`/relative resolution run natively.
  Repo-buildable (`bash napi/build.sh`, `node napi/test.mjs`); publishing
  waits on a per-platform prebuild matrix.

### Changed (npm package — async path performance; released as `sasso@0.8.0` on npm)

- **Concurrent `compileStringAsync`/`compileAsync` calls no longer serialize.**
  The single asyncify-instance lock is replaced by a lazily-grown pool of
  asyncify engines (default cap: `min(4, cpu cores)`, tunable via the new
  `configure({ asyncInstances })`). While one compile awaits an asynchronous
  importer, other compiles run on other engines — a bundler fanning out N
  sass entries no longer queues them end-to-end (measured: N=8 fan-out with
  2 ms importer latency, makespan −75.6%). A process that never overlaps
  async compiles still pays for exactly one instance; each additional engine
  reserves its own wasm memory (incl. the arena) plus a 1 MiB asyncify stack.
- **Synchronously-resolving importers and custom functions no longer pay the
  asyncify suspension on the async APIs.** Results that settle synchronously
  (plain return values — including the built-in `loadPaths` filesystem chain
  and sass-loader's cache-hit resolutions) are delivered without an
  unwind/rewind cycle. A `loadPaths`-only `compileStringAsync` now suspends
  zero times. Genuinely-async importers behave exactly as before.
- **`sasso/speed`'s async APIs now run a speed-optimized (`-O3`) asyncify
  module** (`sasso.speed.async.wasm`, ~3.2 MB / 1.0 MB gzip) instead of
  sharing the size-optimized one — ~2× engine throughput at v8 steady state
  (long-lived processes; one-shot CLI-style runs are dominated by v8 tiering
  and see little change). The default `sasso` entry is unchanged.
- Degraded async modules (built without `wasm-opt`) previously crashed on the
  first importer callback; they now work for synchronously-resolving chains
  and reject genuinely-async importers with a clear error.

### Fixed

- **Real-world corpora now compile — four previously failed.** Callable
  closures capture the defining file's `@use` namespace tables (dart
  `Environment.closure()`), fixing "There is no module with the namespace
  X" for functions/mixins reached via `@import` or multi-hop `@forward`
  (uswds's `units()`, quasar's `str-fe()`). CSS escapes are literal
  identifier text in selector scans (`.govuk-\!-font-size-19`,
  govuk-frontend). In the indented syntax, `as *` terminates a
  `@use`/`@forward` prelude instead of reading as a pending multiplication
  (vuetify), and a trailing-comma selector line with a pseudo-glued colon
  (`&:active,` / `i[type="s"]::-webkit-x,`) continues the list instead of
  being **silently dropped** (quasar — a correctness bug, not formatting).
- **Byte-parity with dart-sass across the serialization surface.** Loud
  comments dedent at serialize time (interpolated banners included) and an
  indented `/**` opener stays glued; comments registered before a module's
  first load re-emit at every dependency edge (bulma's `/* Bulma Form */`);
  `@import`ed files carry their own file identity (no cross-file trailing
  -comment gluing); invisible `@extend`-only rules leave no blank-line group
  end; selector lists keep their authored line structure inside nested
  at-rule wraps and plain-CSS imports (which also unquote identifier
  attribute values like dart's parser); multi-`&` parent expansion
  interleaves column-major (mastodon's adjacent-state selectors); a `&`
  nested in pseudo parens substitutes inside multi-`&` parts
  (`:not(&--mini-animate)`).
- **Chained `@extend` products keep dart's registration order.** Each
  `@extend`'s extender list is pre-extended by the store accumulated so far
  (dart's `addSelector`), so `.navbar > .container, … .container-xxl` comes
  out in forward order instead of reversed.
- **Errors inside loaded files are attributed to that file.** The snippet
  renders from the erring file and the trace stacks one frame per loader
  (`_mod.scss 1:13  @use` / `main.scss 1:1  root stylesheet`), matching
  dart for `@use`, `@forward`, and `@import` chains — parse errors
  included. Previously the root file's name and snippet were shown with the
  inner file's line numbers.
- **`@media` nested inside an unknown at-rule now compiles.** dart's
  `_inUnknownAtRule` context legalizes bare declarations without an enclosing
  style rule, so the canonical Tailwind v4 idiom
  `@utility container { @media (width >= 96rem) { max-width: 87.5rem; } }`
  parses and emits verbatim (byte-matched to dart-sass 1.101, including the
  classic `min-width` syntax, interpolated queries, and mixed
  declaration-plus-`@media` bodies). A bare declaration in a top-level
  `@media` still errors like dart. Previously: `Error: expected "{".`
- **Keyframe selector lists now join on one line, matching dart-sass.** A
  multi-line authored frame selector (`0%,\n60%,\n100% {`) was emitted with
  the author's line breaks preserved, as style-rule selector lists are; dart
  re-serializes keyframe stops joined with `", "` and drops the breaks
  (`0%, 60%, 100% {`). Found compiling a real-world Rails corpus (a Bootstrap
  → Tailwind compat layer) where this was the only byte difference across
  ~132 KB of output.

## [0.6.3] - 2026-06-25

### Fixed

- **Trailing-newline parity with dart-sass, split correctly between the library
  API and the CLI.** The library API (`compile`, `compile_with_source_map`, and
  thus the wasm `compileString().css` and the Ruby gem) now returns the
  serialized stylesheet with **no trailing newline** — byte-for-byte what
  dart-sass's library API returns. Previously expanded output carried a stray
  trailing newline, so a `sass-loader`/Vite integration saw one extra byte per
  stylesheet versus dart-sass. The CLI front-ends (the `sasso` binary and the
  wasm `sasso` CLI) now append the single trailing newline dart-sass's CLI adds
  to **non-empty** output in **both** styles — compressed CLI output previously
  omitted it — while empty output stays empty (0 bytes), also matching
  dart-sass. Verified byte-for-byte against dart-sass 1.101.0 on both the
  library and CLI paths (expanded, compressed, and empty output).
- **The wasm package now reports `dart-sass 1.101.0` in `info`** (was
  `1.89.0`), reflecting the modern-API surface it's compatible with. Tools such
  as `sass-loader` and Vite parse this version for feature gating, so the stale
  value could gate off newer behavior.

## [0.6.2] - 2026-06-25

### Fixed

- **Compressed output now emits the shortest equivalent legacy-color form**,
  matching dart-sass 1.101.0 byte-for-byte. A computed color such as
  `darken(#336699, 10%)` is written as `hsl(210,50%,30%)` instead of the longer
  `rgb(38.25,76.5,114.75)`, and an integer-rgb-equivalent hsl literal
  (`hsl(210, 50%, 40%)`) collapses to `#369`. The serializer now compares the
  hex/name, `rgb()`/`rgba()`, and `hsl()`/`hsla()` candidates and keeps the
  shortest — the rgb form winning ties — while a powerless (zero-saturation)
  hue is preserved. Expanded output is unchanged. This compressed path had no
  cross-check before (the conformance ratchet and the parity suite were both
  expanded-only), so a compressed dart-sass parity battery now runs in
  `tests/parity.rs`.

## [0.6.1] - 2026-06-16

### Fixed

- A relative `meta.load-css` inside a **first-class mixin** (captured with
  `meta.get-mixin` and invoked via `meta.apply`) now resolves against the
  mixin's **defining** file, not the caller's (issue #8). The regular
  namespaced include path was already correct.

### Added

- A **C ABI** (`ffi/`, `libsasso` + `sasso.h`) — drive sasso in-process from any
  language with a C FFI, with a userland importer callback. Releases now attach a
  per-target `sasso-<version>-<target>-c-api.{tar.xz,zip}` (prebuilt static +
  dynamic library + header). See the "C ABI" section in the README.

## [0.6.0] - 2026-06-15

### Changed (breaking)

- **The `Importer` trait is now dart-sass's two-phase `canonicalize`/`load`**
  (issue #4, RFC in `docs/IMPORTER_REDESIGN.md`). It replaces the old
  `resolve(path) -> Option<String>` plus the accreted `resolve_*` overloads:

  ```rust
  fn canonicalize(&self, url: &str, ctx: &CanonicalizeContext)
      -> Result<Option<CanonicalUrl>, ImporterError>;
  fn load(&self, canonical: &CanonicalUrl)
      -> Result<Option<ImporterResult>, ImporterError>;
  ```

  `canonicalize` resolves a URL to a stable identity without loading (its result
  is the module-cache key); `load` fetches the source as an
  `ImporterResult { contents, syntax, source_map_url }`. Three outcomes:
  `Ok(Some)` = handled, `Ok(None)` = not handled, `Err(ImporterError)` =
  handled-but-failed (an actionable compile error rather than a silent miss).
  New public types: `CanonicalUrl`, `ImporterResult`, `ImporterError`,
  `CanonicalizeContext`. A clean break with no compatibility shim (pre-1.0).
  `FsImporter` and the built-in resolution are unchanged in behavior (sass-spec
  ratchet delta +0); only custom `Importer` implementations must migrate.

### Added

- `ImporterResult.source_map_url` lets an importer set the URL recorded for a
  loaded file in generated source maps (dart-sass `ImporterResult.sourceMapUrl`).

## [0.5.3] - 2026-06-15

### Fixed

- **`!default` no longer evaluates its right-hand side when the variable is
  already set.** dart-sass short-circuits a guarded (`!default`) assignment
  *before* evaluating the RHS, so an expression that would otherwise error is
  harmless once the variable already holds a non-null value; sasso evaluated the
  RHS first. This surfaced in Bootstrap-on-Shopware setups where, after an
  override sets `$w: 1rem`, a later `$p: $w + .5em !default` raised an
  "incompatible units" error instead of being skipped. Thanks to
  [@shyim](https://github.com/shyim) (#2).
- **Legacy `rgb()`/`hsl()` preserve the caller's `rgba`/`hsla` spelling in
  special-value and relative-color passthroughs.** When a call can't resolve to
  a concrete color (a channel or alpha is a `var()`/`env()`/non-foldable
  `calc()`), dart-sass keeps the call *and* the exact function name written;
  sasso normalized `rgba`/`hsla` down to `rgb`/`hsl`, breaking Bootstrap's
  `rgba(var(--bs-body-color-rgb), …)` output. The called name is now threaded
  through every passthrough, keeping dart's carve-out that a `none`-only call
  still normalizes to the canonical `rgb`/`hsl` (and a `calc()` alpha that folds
  to a number resolves to a real color rather than a passthrough). Thanks to
  [@shyim](https://github.com/shyim) (#3).

## [0.5.2] - 2026-06-14

### Fixed

- **Expanded `@at-root` group-separation blank lines.** dart-sass writes one
  blank line at an `@at-root` hoist→resume boundary when the hoisted chunk ends
  in a style rule, while keeping a nested-`@at-root` chain and a rule + its own
  bubbled `@media` contiguous. sasso previously diverged BOTH ways — it never
  emitted the blank before a resumed parent rule, and it over-emitted (three
  blanks between top-level bare-`@at-root` siblings, spurious blanks between a
  nested-`@at-root` chain's rules / between a rule and its own bubbled `@media` /
  before an `@at-root` body's trailing comment). Now byte-exact vs dart-sass
  1.101 across a dedicated 54-shape group-separation sweep, with non-`@at-root`
  output byte-identical. Compressed output is unaffected (no blank lines).
  (sass-spec does not cover these `@at-root`-resume blanks.)

## [0.5.1] - 2026-06-14

Source-map fidelity + compressed-output corrections, all byte-exact vs
dart-sass 1.101.

### Fixed

- **Source maps: `@media`/`@at-root`/`@supports` bubbled parent selector.** When
  one of these at-rules nested in a style rule bubbles a copy of the enclosing
  selector out (`@media screen { .a { … } }`), that copy now maps back to the
  ORIGINAL rule's source position, matching dart-sass. It previously had no
  mapping at all — which in compressed output also let the consecutive-same-
  source-line coalescing drop a following declaration's mapping (a 0.5.0
  regression vs 0.4.0 for `@media`/`@at-root`-bubbled rules). CSS is unchanged.
- **Source maps: `@supports` header.** The `@supports (…)` at-rule header now
  maps to its `@supports` keyword (as `@media` already did); previously it had
  no mapping. CSS is unchanged.
- **Compressed `@media`/`@supports` whitespace.** Compressed output now omits the
  space before a prelude beginning with `(` for `@media`/`@supports`
  (`@media(min-width: 1px)`), and within a `@media` query drops the space before
  `and`/`or` after a `)` (`(a)and (b)`) and after the comma between queries
  (`(a),(b)`) — matching dart-sass. Other at-rules (`@container`) and `@supports`
  conditions keep their spaces. (Compressed CSS output change; expanded
  unchanged.)

## [0.5.0] - 2026-06-14

### Added

- **wasm: source maps.** The `@momiji-rs/sasso` package's `compile(scss, {
  sourceMap: true [, sourceMapIncludeSources: true] })` now returns
  `{ css, sourceMap }` (the v3 map as a parsed object) instead of a bare CSS
  string; without `sourceMap` it still returns the string (backwards
  compatible). New `sasso_compile_map` export returns a framed `[u32 css_len][css]
  [map json]` buffer. Source maps are now exposed on every surface (lib, CLI,
  wasm).

### Fixed

- **Compressed source maps** now emit one segment per source line, matching
  dart-sass (compressed packs many tokens onto a line; dart maps only the first
  per source line). Expanded maps are unchanged. The map's CSS is unaffected.

## [0.4.0] - 2026-06-14

### Added

- **Source map (v3) support.** New `compile_with_source_map(source, &Options)
  -> CompileResult { css, source_map: SourceMap }`, with `SourceMap::to_json()`
  and `Options::with_source_map_include_sources(bool)`. The CLI gains
  `-o/--output <file>` (write CSS to a file), `--source-map` (also write a
  `<output>.map` sidecar + append the `sourceMappingURL` footer),
  `--embed-sources`, and `--source-map-urls=relative|absolute`. Output is
  byte-for-byte identical to dart-sass for the common cases (selector +
  declaration-name mappings; expanded + compressed). The plain `compile` path
  and stdout output are unchanged. (Deferred for now: declaration-value-start
  mappings, the inline `--embed-source-map` data URI.)

### Changed

- Internal maintainability refactors only (no behaviour change, byte-identical
  output): the `.sass` line scanners, the `is_builtin` name table, and the
  oversized `eval`/`selector`/`parser` files were split into domain modules;
  the string serializers gained a no-escape fast path.

## [0.3.1] - 2026-06-13

### Fixed

- Compressed output now emits a color's canonical CSS name when it is no longer
  than the shortest hex, matching dart-sass (`red` not `#f00`, `aqua` not
  `#0ff`; duplicate names resolve to dart's canonical pick — `cyan`/`grey` →
  `aqua`/`gray`). Expanded output (which preserves the authored spelling) is
  unchanged.

## [0.3.0] - 2026-06-13

Since `0.2.0`. Conformance holds at **100% of the attempted sass-spec suite**
(13,896 / 13,896) — but that suite covers *valid* inputs plus the errors it
expects; this cycle hardened sasso to reject the same *malformed* inputs
dart-sass rejects, and cut more of the `@extend` and value hot paths.

### Changed

- **Strict input validation.** Beyond matching dart-sass's output, sasso now
  *errors* — rather than silently accepting — on malformed input, each with
  dart-sass's exact message: an invalid hex literal (`#00000`, `#0g`),
  out-of-grammar `rgb()`/`hsl()` channel units and legacy-vs-modern argument
  shapes, a duplicate `@mixin`/`@function` parameter, a malformed number
  exponent (`1e-`), a non-identifier `@use`/`@forward` namespace, a misplaced
  `@content`/`@extend`, a style rule / declaration / `@extend` in a `@function`
  body, a map or empty list used as a CSS value (`#{(a:1)}`, `-()`), a
  malformed `:nth-child()` An+B or empty `:not()` selector, a stray `!` in a
  selector, a leading-empty `@extend` target, and a malformed `@charset` /
  `@at-root (…)` query. Found by a leniency-mining sweep that diffed every
  category against dart-sass; the fixes are uncovered by the spec, so the
  ratchet is unchanged.

### Performance

- **Transitive `@extend`** went from ~151× *slower* than dart-sass to *faster*
  on a deep extend chain: a match pre-filter with typed dedup, an incremental
  per-rule fold (killing the O(N²) closure re-derivation), borrowed
  scope-originals, and cached typed selector hashes — the `@extend` maps are
  now FxHash + typed `Complex`/`Simple` keys, guarded by a render-injectivity
  parity proof. Byte-identical output throughout.
- **Reference-counted composite values** — `Str`/`List`/`Map` are `Rc`-backed,
  so cloning a read-only `$variable` is an O(1) refcount bump instead of a deep
  copy (copy-on-write for the mutating builtins): ~7× fewer instructions and
  ~13× less peak memory when a large list/map is passed through a call chain.
- **`Cow`-borrowed argument-name normalization** (called 4–6× per function
  call) plus trimmed function-call-path allocations — ~15% fewer instructions
  on a function-heavy compile.
- **Arena in-place `realloc`** — the scoped bump arena extends its tail
  allocation in place instead of stranding a dead buffer on every `Vec`
  doubling, trimming peak memory on parse-heavy compiles.
- Net: pure-compile throughput ~7.4 ms on the large benchmark (was ~9–10),
  ~2.3–2.9× faster than `grass` and ~19–30× faster than the dart-sass JS bin.

### Internal

- `eval.rs` split into an `eval/` module directory and `color.rs` into a
  `color/` directory (pure code moves); the typed selector model gained a
  parity-proof harness; the stringly hoist markers became typed `OutNode`
  variants; the `@extend` cartesian-order bool became a `CartesianOrder` enum;
  `OutNode` rule/at-rule constructors collapsed duplicated construction sites.
  All byte-identical, each verified base-binary-vs-refactor.

### Tooling

- The WebAssembly npm package publishes via OIDC Trusted Publishing (no token).
- Benchmark harness uses portable temp-file handling (`mktemp -d`).

## [0.2.0] - 2026-06-11

Everything since the initial `0.1.0` crates.io publish. This grew the compiler
from an early vertical slice to **100% of the *attempted* official sass-spec
suite** (13,896 / 13,896, zero failures — 11,405 byte-exact CSS outputs plus
2,491 error specs correctly rejected; the 8 remaining cases are tagged `:todo`
for dart-sass itself upstream), matching current dart-sass (1.100) byte-for-byte.
The pass is measured against a conformance harness tightened to reproduce the
official sass-spec comparator (`normalizeOutput`) exactly — collapse newline
runs only, no extra whitespace leniency — so the count holds under the upstream
comparator, not just a looser local one.

### Added

- **Byte-exact diagnostics** — errors, `@error`, `@warn`, and `@debug` now
  reproduce dart-sass's stderr byte-for-byte: source-span `╷│╵` snippets with
  carets and right-aligned gutters (tab→4 spaces), aligned stack frames
  (`root stylesheet` / `name()` / `@import`), a `--no-unicode` flag, and the
  `@import` deprecation warning (with a per-id cap/dedup deprecation registry).
  238 of the suite's 3,256 stderr expectations now match byte-for-byte (a
  `spec/run_spec.py --check-stderr` metric tracks it); the rest (other
  deprecations, multi-span layouts) build on this foundation.
- **`@use` / `@forward` module system** — built-in `sass:*` modules and user
  files, `with` configuration, namespacing, `@forward` prefix/`show`/`hide`,
  dash-insensitive member access, forward conflict resolution, and star
  (`as *`) modules.
- **Indented `.sass` syntax** — a full front-end (`Options::with_syntax`, the
  CLI `--indented` flag, `.sass` extension inference), including cross-syntax
  `@import` of partials by file extension.
- **CSS Color 4 color spaces** — `srgb`/`display-p3`/`lab`/`lch`/`oklab`/
  `oklch`/`xyz` via `color()`, with modern color serialization.
- **`@extend` and `%placeholder`s** — a faithful port of dart-sass's
  `ExtensionStore` engine: registration-order extension folding, selector
  weaving/unification/trimming, `@use`/`@forward` cross-module visibility, and
  the self-referential `:not`/`:has` pseudo cases — closing the suite's
  `@extend` family to byte-exact parity.
- **Built-in function modules** — `meta` (first-class function references via
  `get-function`/`call`, existence predicates), `math` (`clamp`/`min`/`max`/
  `round`/`log` and friends), `list` (bracket-preserving `join`/`append`),
  `map` (nested key paths, `deep-merge`/`deep-remove`), `string`
  (`split`/`unique-id`), and `selector` functions.
- **First-class mixins** — `meta.get-mixin` returns a mixin value and
  `meta.apply` invokes it (with `@content` support).
- **CLI** — compile multiple input files in one process (`sasso a.scss b.scss`,
  startup shared across files); `--loop <N>` for in-process throughput and
  `-q`/`--quiet` to suppress stdout (used by the benchmark harness).
- **Benchmark harness** — sasso registered as a first-class engine in `bench/`;
  three-way report [`bench/three_way.md`](bench/three_way.md) (sasso vs
  dart-sass vs grass).
- **`CODE_OF_CONDUCT.md`** adopting the
  [Sass Community Guidelines](https://sass-lang.com/community-guidelines/), as
  the Sass project asks every implementation to do.
- **This CHANGELOG.**

### Changed

- Selector resolution now matches dart-sass on combinator normalization,
  adjacent-compound separation, and bogus-combinator omission.
- `color` functions match dart-sass strictness: channel-unit leniency in
  `adjust`/`change`, missing/powerless-channel errors, the Microsoft `alpha()`
  filter overload, and `adjust-hue` rejecting non-legacy colors.
- `selector` functions coerce string/list arguments and validate arity, and
  accept a list of extendees in `extend`/`replace`.
- `list` builtins validate fixed-arity arguments and preserve list shape.
- Unquoted string serialization collapses newlines to spaces; custom-property
  values are emitted verbatim — both matching dart-sass.
- Control-flow blocks use semi-global scoping with a global-write guard.

### Performance

Profiling showed the compiler is allocation- and hashing-bound; a series of
hot-path cuts followed, with no behavior change (cumulative **~2× faster** on the
large benchmark vs. the original, lifting the lead over `grass` to ~1.9–2.4× and
over dart-sass to ~16–25×):

- Selector helpers `split_commas`/`tokenize_complex` return borrowed `&str`
  slices, and `copy_name`/`normalize_selector` avoid their intermediate
  `String`/`Vec` — no per-part/per-token/per-name heap allocation on the hot
  selector-resolution path.
- The compiler's internal `String`-keyed maps (variable scope, function/mixin
  tables, module maps) use a small inline FxHash hasher instead of std's
  DoS-resistant-but-slow SipHash. (Still zero runtime dependencies.)
- A **scoped bump-arena allocator** (`ScopedAlloc`): within each `compile()` a
  per-thread arena turns every allocation into a pointer bump and frees them
  wholesale (reset) at the end — a further ~1.5×. It is installed as the CLI's
  `#[global_allocator]`; library/wasm embedders can opt in the same way (it
  forwards to the system allocator outside a compile, so it's safe to install
  unconditionally). This is the library's one audited `unsafe` module —
  verified by unit tests, Miri (no UB), AddressSanitizer, and the full sass-spec
  suite run through it (zero crashes, byte-identical output); the
  rest of the crate is `deny(unsafe_code)`. Still zero runtime dependencies.
- **Smaller `Value`** — the `Color` variant's modern-color payload is boxed, so
  the `Value` enum drops from 128 to **64 bytes** (a compile-time `size_of`
  guard prevents regressions). Halves every scope-map slot, `Vec<Value>` element
  and lookup clone, with byte-identical output.
- **Zero-dependency Ryū float formatter** — a from-scratch `d2s` shortest-round-
  trip formatter on the float-to-string hot path, replacing `core::fmt`, with a
  differential fuzz test against a reference.

### Tooling

- The conformance ratchet pins the sass-spec commit (`spec/SPEC_VERSION.txt`)
  for reproducibility, with a `--latest`/`--canary` drift-detection mode.
- The conformance harness now reproduces the official sass-spec comparator
  (`sass-spec/lib/test-case/compare.ts` `normalizeOutput`) exactly, so a "pass"
  means byte-identical under the upstream comparator — no extra local leniency.

## [0.1.0] - 2026-06-06

Initial crates.io publish — an early vertical slice that already compiled
real-world SCSS byte-identically to dart-sass.

### Added

- `$variables` with lexical scoping, `!default` and `!global`.
- Nesting, the `&` parent selector with selector-list multiplication, and
  combinator normalization (`>`, `+`, `~`).
- `#{}` interpolation in selectors, property names and values.
- `//` (stripped) and `/* */` (preserved) comments.
- Numbers with units and unit arithmetic.
- A color model with fractional channels and author-spelling preservation;
  color functions (`rgb`/`rgba`/`hsl`/`hsla`/`mix`/`lighten`/`darken`/
  `percentage`/`red`/`green`/`blue`/`alpha`).
- `@import` partial inlining through a pluggable `Importer` (CSS imports pass
  through); a ready-made `FsImporter`.
- `expanded` and `compressed` output styles.
- Distribution: CLI binary (prebuilt via cargo-dist), library crate, and a
  zero-dependency WebAssembly build published to npm as `@momiji-rs/sasso`.

[Unreleased]: https://github.com/momiji-rs/sasso/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/momiji-rs/sasso/compare/v0.6.3...v0.7.0
[0.3.0]: https://github.com/momiji-rs/sasso/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/momiji-rs/sasso/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/momiji-rs/sasso/releases/tag/v0.1.0
