Skip to content
Merged
18 changes: 18 additions & 0 deletions kyo-data/shared/src/main/scala/kyo/ConstValue.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kyo

import scala.compiletime.constValue as scConstValue

/** An opaque type that materializes a compile-time constant value as a runtime value via an implicit given.
*
* `ConstValue[A]` is a subtype of `A`, backed by `scala.compiletime.constValue`. When `A` is a singleton literal type (e.g., `"name"`,
* `42`, `true`), summoning a `ConstValue[A]` produces the corresponding runtime value. This is useful for passing singleton type
* information to runtime code without requiring an explicit `inline` context at every call site.
*
* @tparam A
* A singleton literal type whose compile-time value is materialized at runtime
*/
opaque type ConstValue[A] <: A = A

object ConstValue:
/** Materializes the compile-time constant for singleton type `A` as a runtime value. */
inline given [A]: ConstValue[A] = scConstValue[A]
34 changes: 34 additions & 0 deletions kyo-data/shared/src/main/scala/kyo/Field.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kyo

import Record.~

/** A reified field descriptor carrying the field's singleton string name, its value type's `Tag`, and optional nested field descriptors
* (populated when the value type is itself a `Record`).
*
* Field instances are typically obtained from `Fields.fields` or constructed via the companion's `apply` method. They serve as runtime
* metadata for operations like `mapFields`, `stage`, and serialization, and also provide typed `get`/`set` accessors on records.
*
* @tparam Name
* The singleton string type of the field name
* @tparam Value
* The field's value type
*/
case class Field[Name <: String, Value](
name: Name,
tag: Tag[Value],
nested: List[Field[?, ?]] = Nil
):
/** Extracts this field's value from a record. Requires evidence that `F` contains `Name ~ Value`. */
def get[F](record: Record[F])(using F <:< (Name ~ Value)): Value =
record.toDict(name).asInstanceOf[Value]

/** Returns a new record with this field's value replaced. Requires evidence that `F` contains `Name ~ Value`. */
def set[F](record: Record[F], value: Value)(using F <:< (Name ~ Value)): Record[F] =
Record.init(record.toDict.update(name, value.asInstanceOf[Any]))
end Field

object Field:
/** Constructs a `Field` by summoning the singleton name value and `Tag` from implicit scope. */
def apply[Name <: String & Singleton, Value](using name: ConstValue[Name], tag: Tag[Value]): Field[Name, Value] =
Field(name, tag)
end Field
144 changes: 144 additions & 0 deletions kyo-data/shared/src/main/scala/kyo/Fields.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package kyo

import kyo.Record.*
import scala.compiletime.*

/** Reifies the structure of an intersection of `Name ~ Value` field types into runtime metadata and type-level operations.
*
* Given a field type like `"name" ~ String & "age" ~ Int`, a `Fields` instance decomposes it into a tuple of individual field components
* (`("name" ~ String) *: ("age" ~ Int) *: EmptyTuple`) and provides both runtime access (field names, `Field` descriptors) and type-level
* transformations (mapping, value extraction, zipping). Also supports case class types by deriving the equivalent field intersection from
* the product's element labels and types.
*
* Instances are derived transparently via a macro (`Fields.derive`), and are summoned implicitly by `Record` operations like `map`,
* `compact`, `values`, and `zip`.
*
* @tparam A
* The field intersection type (e.g., `"name" ~ String & "age" ~ Int`) or a case class type
*/
sealed abstract class Fields[A] extends Serializable:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the replacement for TypeIntersection


/** Tuple representation of the fields: `"a" ~ Int & "b" ~ String` becomes `("a" ~ Int) *: ("b" ~ String) *: EmptyTuple`. */
type AsTuple <: Tuple

/** Applies a type constructor `F` to each field component and re-intersects the results. For example,
* `Fields["a" ~ Int & "b" ~ String].Map[Option]` yields `Option["a" ~ Int] & Option["b" ~ String]`.
*/
type Map[F[_]] = Fields.Join[Tuple.Map[AsTuple, F]]

/** Extracts the value types from each field component into a plain tuple: `Fields["a" ~ Int & "b" ~ String].Values` = `(Int, String)`.
*/
type Values = Fields.ExtractValues[AsTuple]

/** Zips this field tuple with another by name, pairing their value types into tuples. */
type Zipped[T2 <: Tuple] = Fields.ZipValues[AsTuple, T2]

/** Runtime `Field` descriptors (name, tag, nested), lazily materialized. */
lazy val fields: List[Field[?, ?]]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only materializes fields if necessary


/** The set of field names, derived from `fields`. */
def names: Set[String] = fields.iterator.map(_.name).toSet

end Fields

/** Companion providing derivation, type-level utilities, and evidence types for field operations. */
object Fields:

private[kyo] def createAux[A, T <: Tuple](_fields: => List[Field[?, ?]]): Fields.Aux[A, T] =
new Fields[A]:
type AsTuple = T
lazy val fields = _fields

private[kyo] type Join[A <: Tuple] = Tuple.Fold[A, Any, [B, C] =>> B & C]

/** Match type that extracts value types from a tuple of field components into a plain tuple. */
type ExtractValues[T <: Tuple] <: Tuple = T match
case EmptyTuple => EmptyTuple
case (n ~ v) *: rest => v *: ExtractValues[rest]

/** Match type that looks up the value type for field name `N` in a tuple of field components. */
type LookupValue[N <: String, T <: Tuple] = T match
case (N ~ v) *: _ => v
case _ *: rest => LookupValue[N, rest]

/** Match type that zips two field tuples by name, pairing their value types into `(V1, V2)` tuples. */
type ZipValues[T1 <: Tuple, T2 <: Tuple] = T1 match
case EmptyTuple => Any
case (n ~ v1) *: EmptyTuple => n ~ (v1, LookupValue[n, T2])
case (n ~ v1) *: rest => (n ~ (v1, LookupValue[n, T2])) & ZipValues[rest, T2]

/** Refinement type alias that exposes the `AsTuple` member. */
type Aux[A, T] =
Fields[A]:
type AsTuple = T

/** Macro-derived given that produces a `Fields` instance for any field intersection type or case class. */
transparent inline given derive[A]: Fields[A] =
${ internal.FieldsMacros.deriveImpl[A] }

/** Returns the `Field` descriptors for type `A`. Convenience accessor for `summon[Fields[A]].fields`. */
def fields[A](using f: Fields[A]): List[Field[?, ?]] = f.fields

/** Returns the field names for type `A`. Convenience accessor for `summon[Fields[A]].names`. */
def names[A](using f: Fields[A]): Set[String] = f.names

/** An opaque map from field name to a type class instance `F[Any]`, summoned inline for each field's value type. Used by operations
* like `Render` that need a type class instance per field.
*/
opaque type SummonAll[A, F[_]] = Map[String, F[Any]]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simpler replacement for Inliner

Copy link
Contributor

@road21 road21 Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guess Inliner was slightly more powerful, but this api (without having to extend class) is much more user friendly


extension [A, F[_]](sa: SummonAll[A, F])
/** Retrieves the type class instance for the given field name. */
def get(name: String): F[Any] = sa(name)

/** Returns true if an instance exists for the given field name. */
def contains(name: String): Boolean = sa.contains(name)

/** Iterates over all (name, instance) pairs. */
def foreach(fn: (String, F[Any]) => Unit): Unit = sa.foreach((k, v) => fn(k, v))
end extension

object SummonAll:
/** Inline given that summons `F[V]` for each field `Name ~ V` in `A` and collects them into a map keyed by field name. Fails at
* compile time if any field's value type lacks an `F` instance.
*/
inline given [A, F[_]](using f: Fields[A]): SummonAll[A, F] =
summonLoop[f.AsTuple, F]

private inline def summonLoop[T <: Tuple, F[_]]: Map[String, F[Any]] =
inline erasedValue[T] match
case _: EmptyTuple => Map.empty
case _: ((n ~ v) *: rest) =>
summonLoop[rest, F].updated(constValue[n & String], summonInline[F[v]].asInstanceOf[F[Any]])
end SummonAll

/** Evidence that field type `F` contains a field named `Name`. The dependent `Value` member resolves to the field's value type. Used by
* `Record.selectDynamic` and `Record.getField` to verify field access at compile time and infer the return type.
*/
sealed abstract class Have[F, Name <: String]:
type Value

object Have:
private[kyo] def unsafe[F, Name <: String, V]: Have[F, Name] { type Value = V } =
new Have[F, Name]:
type Value = V

/** Macro-derived given that resolves the value type for `Name` in `F`. Fails at compile time if the field does not exist. */
transparent inline given [F, Name <: String]: Have[F, Name] =
${ internal.FieldsMacros.haveImpl[F, Name] }
end Have

/** Opaque evidence that all value types in field type `A` have `CanEqual` instances, enabling `==` comparisons on `Record[A]`. */
opaque type Comparable[A] = Unit

object Comparable:
private[kyo] def unsafe[A]: Comparable[A] = ()

/** Macro-derived given that verifies all field value types in `A` have `CanEqual`. Fails at compile time if any field type lacks
* `CanEqual`.
*/
transparent inline given derive[A]: Comparable[A] =
${ internal.FieldsMacros.comparableImpl[A] }
end Comparable

end Fields
Loading