// SPDX-License-Identifier: MPL-2.0
// Warmup 04: Row polymorphism
//
// Row polymorphism lets you write functions that work on any record with at
// least the fields you need.  It's the principled version of duck typing —
// with compile-time guarantees and no runtime surprises.
//
// Run: affinescript check 04_rows.affine

// ── Basic row-polymorphic function ────────────────────────────────────────────
//
// `[..r]` introduces a row variable.  The function accepts any record that
// has an `x: Int` field, plus any other fields captured by `..r`.

fn get_x[..r](point: {x: Int, ..r}) -> Int {
  point.x
}

// Both of these call sites work:
type Point2D = {x: Int, y: Int}
type Point3D = {x: Int, y: Int, z: Int}

let p2 = Point2D { x: 1, y: 2 }
let p3 = Point3D { x: 3, y: 4, z: 5 }

let x2 = get_x(p2)             // fine — {x: Int, y: Int} has x
let x3 = get_x(p3)             // fine — {x: Int, y: Int, z: Int} has x

// ── Extending a record ────────────────────────────────────────────────────────
//
// The spread syntax `..record` copies all fields of record into the new one.

fn with_id[..r](record: {..r}, id: Int) -> {id: Int, ..r} {
  { id: id, ..record }
}

let tagged_point = with_id(p2, 99)  // { id: 99, x: 1, y: 2 }

// ── Named row type aliases ────────────────────────────────────────────────────
//
// You can name row constraints as type aliases:

type HasName[..r] = {name: String, ..r}
type HasAge[..r]  = {age: Int, ..r}

fn greet_named[..r](entity: HasName[..r]) -> String {
  "Hello, " ++ entity.name ++ "!"
}

// ── Row polymorphism in effects ───────────────────────────────────────────────
//
// Effect rows work the same way.  `/ e` is a row variable over effects.

effect IO { fn println(s: String); }
effect Log { fn warn(s: String); }

fn run_with_log[..e](f: () -> () / IO + ..e) -> () / IO + Log + ..e {
  Log.warn("starting");
  f();
  Log.warn("done");
}

// ── Exercise 1: write a function `swap_x_y` that takes any record with both ───
//    `x: Int` and `y: Int` fields and returns one with the values swapped.
//    Hint: the return type has the same row variable as the input.
// fn swap_x_y[..r](p: {x: Int, y: Int, ..r}) -> {x: Int, y: Int, ..r} { ... }

// ── Exercise 2: write a function `merge` that takes two single-field records ──
//    and produces a record containing both fields.
// fn merge_name_age(named: {name: String}, aged: {age: Int}) -> {name: String, age: Int} { ... }
