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
67 changes: 66 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.0] - 2026-10-25

**Binary-breaking: recompile against this release.** 1.0.0 changes public shapes that 0.18.x
call sites were compiled against: `Optional` is a sealed abstract class whose `getOrModify` /
`reverseGet` are methods (no longer constructor `val`s), `Optic.all` returns `List[A]`, and the
legacy MultiFocus Grate API is removed (all under Changed and Removed). eo's `inline` methods
splice their bodies into YOUR bytecode, so a jar built against 0.18.x keeps calling the old
members and fails at runtime (`NoSuchMethodError` and friends) against 1.0.0, even where the
source still compiles unchanged. Rebuild everything that depends on cats-eo. 1.0.0 is also built
with Scala 3.9.0, whose TASTy (28.9) older compilers cannot read: **downstream builds need Scala
3.9.x or later.**

**What 1.0.0 means: binary compatibility from here on.** MiMa now checks every published module
on every build, so each later 1.x release stays binary-compatible with 1.0.0; breaking changes
wait for 2.0. One exception: **`cats-eo-kyo` is experimental.** It builds on kyo `1.0.0-RC7`, a
release candidate, so it is still published but excluded from the MiMa check and may change in
any 1.x release; the exclusion lifts once kyo 1.0.0 final ships (see the
[Kyo integration](https://eo.constructive.dev/integrations/kyo.html) page). The 0.x releases made
no compatibility promise.

### Added

- **iron: new `cats-eo-iron` module** — [Iron](https://github.com/Iltotore/iron) refined types as
Expand Down Expand Up @@ -70,9 +90,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`JsonSlice(chunk.toByteBuffer)`, `Wire.detail` is `JsoniterPrism.inPlace[Issue].field(_.detail).raw`
with `DetailJson` an opaque `JsonSlice`, the leaf prisms are in place too, and `joinFrames`
takes `Chunk[Byte]`.
- **laws: multi-shot `ForgetfulTraverse` laws and D2 `modifyA[List]` (#145).** Every
`ForgetfulTraverse` / `modifyA` law used to run under a single-shot effect (`Id`, `Const`), so
none could notice a carrier that shares state across an effect's continuations (the PSVec bug
under Fixed). `ForgetfulTraverseLaws` gains sequential composition (`Nested[List, Option]`),
parallel composition (`Tuple2K[List, Vector]`) and `traverseMultiShotAgreesWithFoci`
(`traverse[List]` agrees with cats' `Traverse[List]` over the carrier's foci), in a new
`forgetfulTraverseMultiShot` rule set (its parent, `forgetfulTraverse`, is unchanged).
`ModifyAListLaws` / `ModifyAListTests` (D2) state `modifyA[List] ==` cats' cartesian traverse
over the foci. Multi-shot results grow as `2^n` in the foci, so their generators draw at
ScalaCheck size 8. Additive; downstream suites opt in by registering the new rule sets.
- **docs: [Streaming with fs2](https://eo.constructive.dev/cookbook/streaming-fs2.html) cookbook
page (#142).** Why there is no `cats-eo-fs2` module: the pipes it would ship are one-liners
over fs2's own combinators. Keeping prism matches (written against `CanGetOption`), effectful
modify with `evalMap` + `modifyA` (and why `Stream` as the `Applicative` gives a cartesian
product), routing on a prism's `Either` with `broadcastThrough`, and a `zoomPipe` that runs a
`Pipe` on one field (correct only for one-to-one, in-order pipes). No published module depends
on fs2.
- **demos: two worked programs built from eo optics, each with a cookbook page.**
[Blockchain smart contract compilation](https://eo.constructive.dev/cookbook/uplc-jvm.html)
(#132) compiles UPLC (Cardano Plutus Core) to JVM bytecode as a chain of lawful prisms and an
iso, checked differentially against the Scalus CEK machine.
[Allocation-free stream triage](https://eo.constructive.dev/cookbook/grpc-filter.html) (#133)
filters a gRPC stream of JSON messages read as raw bytes with eo-jsoniter prisms, without
decoding them. Demos are not published.

### Changed

- **`cats-eo-kyo` is experimental.** It is published as before, but outside the 1.x
binary-compatibility promise while it builds on kyo `1.0.0-RC7` (see the lead paragraph).
- **Toolchain and dependencies (#122, #149).** Built with Scala 3.9.0 (was 3.8.4; downstream
needs 3.9.x, see above), which kyo `1.0.0-RC7` requires. Runtime dependency bumps:
jsoniter-scala `2.38.17` → `2.41.2`, kyo-prelude / kyo-schema `1.0.0-RC6` → `1.0.0-RC7`, zio
`2.1.24` → `2.1.26`, zio-prelude `1.0.0-RC41` → `1.0.0-RC48`, vulcan (`Optional`) `1.13.0` →
`1.14.1`, and scalacheck `1.19.0` → `1.20.0` in `cats-eo-laws`.

- **core: `Optional` tears its source once per write** (breaking). `Optional` is now a sealed
abstract class with an existential intermediate `type M` (`X = (T, M)`): a hit keeps what the
read computed, and the write rebuilds from it (`rebuild(m, b)`) instead of re-running the
Expand Down Expand Up @@ -123,6 +175,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **core: `PSVec`'s `Traverse` no longer shares one result array across `G`'s continuations
(#145).** `PSVec.pSVecTraverse.traverse` seeded `G.pure(new Array[Any](n))` and wrote into
that one array from every `G.map2` continuation. Any `Applicative` that resumes a continuation
more than once (`List`, `Vector`, `LazyList`, `Chain`, an fs2 `Stream`, a product effect such as
`Tuple2K`, a re-run `State` or `IO` value) handed every branch the same array, so the last
writer won: `Traversal.each[List, Int].modifyA[List](a => List(a, -a))(List(1, 2, 3))` gave 8
copies of `List(-1, -2, -3)`. Results still land in one pre-sized array, now allocated inside
the first element's continuation (fresh per branch and per run) with a claim frontier: the
first writer at an index writes in place, any later one copies the prefix into its own buffer.
Single-shot effects cost the same as before; multi-shot effects pay one prefix copy per extra
branch. Every `modifyA` over a PSVec-backed traversal (`Traversal.each`, `MultiFocus[PSVec]`)
was affected. The new multi-shot laws under Added pin it.
- **core: `MultiFocus[List].collectList` preserves observed context.**
It passes a singleton focus vector to reconstruction instead of an
invented leftover. This is not a guarantee of a singleton reconstructed
Expand Down Expand Up @@ -1145,7 +1209,8 @@ JsonTraversal×Review corner). See:
[`docs/research/2026-04-23-composition-gap-analysis.md`](docs/research/2026-04-23-composition-gap-analysis.md)
§7 (and the per-cell ledger in §1.1 / §3 / §4).

[Unreleased]: https://github.com/Constructive-Programming/eo/compare/v0.18.0...HEAD
[Unreleased]: https://github.com/Constructive-Programming/eo/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/Constructive-Programming/eo/compare/v0.18.0...v1.0.0
[0.18.0]: https://github.com/Constructive-Programming/eo/compare/v0.17.0...v0.18.0
[0.17.0]: https://github.com/Constructive-Programming/eo/compare/v0.16.0...v0.17.0
[0.16.0]: https://github.com/Constructive-Programming/eo/compare/v0.15.1...v0.16.0
Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ libraryDependencies += "dev.constructive" %% "cats-eo-avro" % "0.18.0"
libraryDependencies += "dev.constructive" %% "cats-eo-jsoniter" % "0.18.0"
libraryDependencies += "dev.constructive" %% "cats-eo-iron" % "0.18.0"
libraryDependencies += "dev.constructive" %% "cats-eo-zio" % "0.18.0"
libraryDependencies += "dev.constructive" %% "cats-eo-kyo" % "0.18.0" // JDK 25
libraryDependencies += "dev.constructive" %% "cats-eo-kyo" % "0.18.0" // JDK 25; experimental
libraryDependencies += "dev.constructive" %% "cats-eo-chimney" % "0.18.0"
```

Requires Scala 3.8.x on JDK 17, 21, or 25 — except `cats-eo-kyo`,
Requires Scala 3.9.x on JDK 17, 21, or 25 — except `cats-eo-kyo`,
which requires JDK 25 (kyo 1.0.0-RC5+ ships Java-25-only bytecode).

## 60-second tour
Expand Down Expand Up @@ -139,7 +139,8 @@ personStreet.modify(_.toUpperCase)(alice) // address.street := "MAIN ST"
`Record.iso` / `Record.lens` for kyo Records, and the optional
kyo-schema bridge: `Focus` → optics, codec byte-face prisms, and
optics over the untyped `Structure.Value` tree (`cats-eo-kyo`,
JDK 25).
JDK 25). **Experimental**: built on kyo 1.0.0-RC7, so it is outside
the 1.x binary-compatibility promise until kyo 1.0.0 final ships.
- [chimney integration](https://eo.constructive.dev/integrations/chimney.html) —
chimney's derived conversions as optics: `Transformer` → `Getter`,
`Iso` → `Iso`, `Codec` → prism, `PartialTransformer` → a fold into
Expand Down Expand Up @@ -174,11 +175,13 @@ for cats typeclasses.

## Project status

`0.1.0` is the first public release. Binary compatibility is guaranteed
within the `0.1.x` series via MiMa starting at `0.1.1` (the `0.1.0`
publish has no prior version to compare against). Pre-`1.0`, expect
surface refinements as we learn from real users; each non-bugfix release
will list breaking changes in [`CHANGELOG.md`](./CHANGELOG.md).
`1.0.0` is the first stable release. From here on, binary compatibility
is checked by MiMa across the whole `1.x` series: every `1.x` release is
binary-compatible with `1.0.0`. The exception is `cats-eo-kyo`, which is
**experimental** and outside that check while it builds on a kyo release
candidate; the exclusion lifts once kyo 1.0.0 final ships. The `0.x`
releases made no compatibility promise. Every release lists its changes
in [`CHANGELOG.md`](./CHANGELOG.md).

## License

Expand Down
12 changes: 10 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ val scala3Version = "3.9.0"
// fields below are ThisBuild-scoped so every published sub-module
// inherits them.

// Breaking-change history per 0.x minor lives in mima.sbt.
ThisBuild / tlBaseVersion := "0.18"
// 1.x is binary-compatible (MiMa-checked from 1.0.0; see mima.sbt, which
// also keeps the 0.x breaking-change history).
ThisBuild / tlBaseVersion := "1.0"
ThisBuild / organization := "dev.constructive"
ThisBuild / organizationName := "Constructive"
ThisBuild / startYear := Some(2025)
Expand Down Expand Up @@ -964,6 +965,13 @@ lazy val kyoIntegration: Project = project
// kyo 1.0.0-RC5+ is Java-25-only bytecode, so this artifact honestly
// targets 25 too (the ThisBuild floor stays 17 for every other module).
tlJdkRelease := Some(25),
// EXPERIMENTAL: no binary-compatibility promise. cats-eo-kyo builds on
// kyo 1.0.0-RC7, a release candidate whose own API and bytecode still
// move between RCs, so this module is excluded from the 1.x MiMa check
// (every other published module is checked against 1.0.0). Lift this
// override — let sbt-typelevel compute the previous versions again —
// once kyo 1.0.0 final ships and the module is rebuilt on it.
tlMimaPreviousVersions := Set.empty,
libraryDependencies += cats,
libraryDependencies += kyoPrelude,
libraryDependencies += kyoSchema % Optional,
Expand Down
82 changes: 42 additions & 40 deletions mima.sbt
Original file line number Diff line number Diff line change
@@ -1,39 +1,54 @@
// MiMa setup — binary-compatibility checking, wired transitively
// through sbt-typelevel-ci-release.
//
// MiMa is currently DISABLED across the still-evolving 0.x line:
// every minor so far has been a deliberate breaking release, and
// cats-eo-avro has no published baseline anyway. Breaking-change
// history, newest first:
// MiMa is ON from 1.0.0. There is no `tlMimaPreviousVersions` override
// here: sbt-typelevel's default computes the previous versions from git
// tags — every earlier non-prerelease `vX.Y.Z` tag that early-semver says
// the current version must stay binary-compatible with (same major, and
// major > 0). So 1.0.0 itself compares against nothing (every earlier tag
// is 0.x), and every later 1.x release is checked against 1.0.0 and each
// 1.x release after it. `mimaReportBinaryIssues` runs in CI.
//
// Unreleased: `Optional` becomes a sealed abstract class with an
// Exception: `cats-eo-kyo` is EXPERIMENTAL and opts out
// (`kyoIntegration / tlMimaPreviousVersions := Set.empty` in build.sbt):
// it builds on kyo 1.0.0-RC7, a release candidate. Lift that override once
// kyo 1.0.0 final ships.
//
// When a 1.x change trips MiMa, the default answer is: don't make it on
// 1.x. Accompany a genuinely internal change with a justified
// ProblemFilter, e.g.
//
// ThisBuild / mimaBinaryIssueFilters ++= Seq(
// ProblemFilters.exclude[MissingClassProblem]("dev.constructive.eo.internal.Foo"),
// )
//
// Only exclude symbols that are genuinely internal (`private` or
// `private[pkg]`); exclusions on the public API are binary-compat
// breakage and belong in a major version bump instead.
//
// History. MiMa was disabled across the 0.x line: every 0.x minor was a
// deliberate breaking release, kept here for reference, newest first.
//
// 1.0.0 (breaking vs 0.18; the changes listed for 0.19 before 1.0 was
// chosen, which was never released, ship here):
// (a) `Optional` becomes a sealed abstract class with an
// existential intermediate (`type M`, `X = (T, M)`) so every write
// tears its source once; `new Optional(...)` is gone (use
// `Optional.apply`) and `getOrModify` / `reverseGet` are methods
// instead of constructor vals (source-compatible for callers,
// binary-breaking).
// Unreleased: remove the legacy MultiFocus Function1 Grate kernel, tuple /
// representable factories, zip / zipWith, and Direct bridge, together
// with RepresentativeIndex. Indexed is the fixed-index replacement;
// composition addresses the full product grid, not the old diagonal.
// Source- and binary-breaking for callers of the removed APIs. Container
// MultiFocus and Traversal APIs are retained.
//
// 0.19: the `Iso → MultiFocus[Function1[X0, *]]` bridge
// (`forgetful2multifocusFunction1`) now requires
// `data.RepresentativeIndex[X0]` — the index its product's own `from`
// reads a bundle at, where a `null` sentinel used to stand in. `Int` /
// `Boolean` / `Unit` / singleton indices resolve off the companion
// (unchanged call sites), any other index type needs a local `given`
// or `RepresentativeIndex.at(v)`, and an uninhabited index type no
// longer bridges (source-breaking for direct references to the given,
// and for chains over an index type with no instance).
// 0.19: core `MultiFocus.representableAt` removed — the `repr0` argument
// never reached the built optic (the factory tabulates pointwise, so
// the index is a read-time argument, not a property of the optic),
// and `MultiFocus.representable` already built the identical carrier.
// Read a position with the `.at(i)` extension instead (source- and
// binary-breaking for direct callers; the replacement is one line).
// (b) the legacy MultiFocus Function1 Grate kernel is removed —
// tuple / representable / representableAt factories, zip /
// zipWith, the Direct bridge (`forgetful2multifocusFunction1`) and
// the short-lived `RepresentativeIndex` witness. Indexed is the
// fixed-index replacement; its composition addresses the full
// product grid, not the old diagonal. Source- and binary-breaking
// for callers of the removed APIs. Container MultiFocus and
// Traversal APIs are retained.
// (c) `Optic.all` returns `List[A]` (every focus) instead of
// `List[F[X, A]]` (binary-breaking descriptor change).
// (d) built with Scala 3.9.0: the TASTy is 28.9, so downstream
// compilers older than 3.9 cannot read it.
// 0.17: (a) jsoniter's string-path constructors keep the failure in the type
// — `JsoniterPrism.fromPath` / `JsoniterTraversal.fromPath` return
// `Either[String, _]`, the throwing `fromPath` is gone, and
Expand Down Expand Up @@ -61,16 +76,3 @@
// enclosing path (namespace) — a wire-format break vs 0.5.x.
// Earlier: 0.2.0 JsonPrism → Affine `Optional` #31, 0.3.0 avro
// field-naming #35, 0.4.0 additive #37/#38, 0.5.0 Confluent surface #41.
//
// Re-enable once the API is stable: set this to the published baseline
// line, and accompany every subsequent binary-breaking change with a
// justified ProblemFilter, e.g.
//
// ThisBuild / mimaBinaryIssueFilters ++= Seq(
// ProblemFilters.exclude[MissingClassProblem]("dev.constructive.eo.internal.Foo"),
// )
//
// Only exclude symbols that are genuinely internal (`private` or
// `private[pkg]`); exclusions on the public API are binary-compat
// breakage and belong in a version bump instead.
ThisBuild / tlMimaPreviousVersions := Set.empty
4 changes: 2 additions & 2 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2")
// signs artifacts with the configured GPG key, and publishes on
// `v*` tag push. Brings MiMa in through its transitive
// `sbt-typelevel-mima` dependency so binary-compat checks run on
// every CI build from 0.1.1 onward (0.1.0 has no previous version
// to compare against; see `mima.sbt`).
// every CI build across 1.x (1.0.0 has no previous 1.x version to
// compare against; 0.x was unchecked; see `mima.sbt`).
addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % "0.8.7")

// `sbt-typelevel-settings` contributes the curated scalac flag set
Expand Down
2 changes: 1 addition & 1 deletion site/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ libraryDependencies ++= Seq(
)
```

Requires Scala 3.8.x on JDK 17 or JDK 21.
Requires Scala 3.9.x on JDK 17, 21, or 25 (`cats-eo-kyo` needs JDK 25).

## Define

Expand Down
12 changes: 10 additions & 2 deletions site/docs/integrations/kyo.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ applications alike.
libraryDependencies += "dev.constructive" %% "cats-eo-kyo" % "@VERSION@"
```

**Experimental.** `cats-eo-kyo` is published with every release but sits
outside eo's 1.x binary-compatibility promise: it builds on kyo
`1.0.0-RC7`, a release candidate, so its API and bytecode can change in
any 1.x release, and MiMa does not check it. Every other eo module is
MiMa-checked across 1.x. The exclusion lifts once kyo 1.0.0 final ships
and the module is rebuilt on it. It also needs JDK 25 (see
[JDK requirement](#jdk-requirement)).

## Which direction are you integrating?

The module is a two-way adapter; pick the row that matches what you
Expand Down Expand Up @@ -253,8 +261,8 @@ pull kyo-schema transitively; add it (and a codec artifact, here json)
yourself:

```scala
libraryDependencies += "io.getkyo" %% "kyo-schema" % "1.0.0-RC6"
libraryDependencies += "io.getkyo" %% "kyo-schema-json" % "1.0.0-RC6"
libraryDependencies += "io.getkyo" %% "kyo-schema" % "1.0.0-RC7"
libraryDependencies += "io.getkyo" %% "kyo-schema-json" % "1.0.0-RC7"
```

### Focus bridge
Expand Down