// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A mapping that remembers the order its members arrived in, because a
/// configuration file does: an INI file written back with its sections sorted
/// alphabetically is a different file, and a person diffing it sees noise
/// instead of their change.
pub(all) struct Table {
/// The members, in the order they were written.
entries : Array[(String, Node)]
} derive(Eq, Debug)
///|
/// An empty mapping.
pub fn Table::new() -> Table {
{ entries: [], }
}
///|
/// How many members the mapping has.
pub fn Table::length(self : Table) -> Int32 {
self.entries.length()
}
///|
/// Whether the mapping has no members.
pub fn Table::is_empty(self : Table) -> Bool {
self.entries.is_empty()
}
///|
/// The member called `key`, or nothing.
pub fn Table::get(self : Table, key : String) -> Node? {
for entry in self.entries {
if entry.0 == key {
return Some(entry.1)
}
}
None
}
///|
/// Whether `key` has a member.
pub fn Table::has(self : Table, key : String) -> Bool {
self.get(key) is Some(_)
}
///|
/// Put `value` under `key`, in the place `key` already holds if it holds one.
///
/// Keeping the position is what makes a merge readable: the winner of a clash
/// stays where the losing value was, so the merged document still reads in the
/// order a person wrote it.
pub fn Table::set(self : Table, key : String, value : Node) -> Unit {
let mut i = 0
while i < self.entries.length() {
if self.entries[i].0 == key {
self.entries[i] = (key, value)
return
}
i += 1
}
self.entries.push((key, value))
}
///|
/// Drop `key`, giving back what it held.
pub fn Table::remove(self : Table, key : String) -> Node? {
let mut i = 0
while i < self.entries.length() {
if self.entries[i].0 == key {
let gone = self.entries[i].1
self.entries.remove(i) |> ignore
return Some(gone)
}
i += 1
}
None
}
///|
/// The members in the order they were written.
pub fn Table::items(self : Table) -> ArrayView[(String, Node)] {
self.entries[:]
}
///|
/// The names, in the order they were written.
pub fn Table::keys(self : Table) -> Array[String] {
self.entries.map(fn(entry) { entry.0 })
}
///|
/// A copy, one level deep: the members are shared, the mapping is not.
pub fn Table::copy(self : Table) -> Table {
{ entries: self.entries.copy(), }
}
///|
/// A value in a configuration tree.
///
/// Numbers keep their kind: an integer stays an integer so that a port number
/// written as `8080` is not printed back as `8080.0`, and so that a binding to
/// an `Int` field can accept it without guessing.
pub(all) enum Value {
/// Nothing was written, and nothing was meant.
Null
/// `true` or `false`.
Bool(Bool)
/// A whole number.
Int(Int64)
/// A number with a fractional part.
Real(Double)
/// Text, however it was quoted.
Text(String)
/// An ordered sequence.
List(Array[Node])
/// A mapping, in the order its members were written.
Map(Table)
} derive(Eq, Debug)
///|
/// A value and the provenance that makes an error message usable: which source
/// it came from, and where in that source it was written.
pub(all) struct Node {
/// The value itself.
value : Value
/// The name the caller gave the source — a file name, `env`, `defaults`.
source : String
/// Where in that source the value was written.
at : At
} derive(Eq, Debug)
///|
/// A node of any value, with the source it came from and where in it.
pub fn Node::new(value : Value, source : String, at : At) -> Node {
{ value, source, at, }
}
///|
/// The same value, said to come from somewhere else. A merge uses this to
/// remember which layer won.
pub fn Node::from(self : Node, source : String, at? : At) -> Node {
{ value: self.value, source, at: at.unwrap_or(self.at), }
}
///|
/// The name of the shape a value has, for error messages: `text`, `integer`,
/// `real`, `boolean`, `null`, `list`, `mapping`.
pub fn Node::kind(self : Node) -> String {
match self.value {
Null => "null"
Bool(_) => "boolean"
Int(_) => "integer"
Real(_) => "real"
Text(_) => "text"
List(_) => "list"
Map(_) => "mapping"
}
}
///|
/// The text, or a [`Type`] refusal naming what was there instead.
pub fn Node::text(self : Node) -> String raise Refused {
match self.value {
Text(text) => text
_ =>
raise Type(
want="text",
got=self.kind(),
path="",
source=self.source,
at=self.at,
)
}
}
///|
/// The integer, or a [`Type`] refusal. A real with no fractional part is
/// accepted, because `8080.0` and `8080` are the same port.
pub fn Node::int(self : Node) -> Int64 raise Refused {
match self.value {
Int(n) => n
Real(x) =>
if x == x.trunc() && x.abs() < 9.007199254740992e15 {
x.to_int64()
} else {
raise Type(
want="integer",
got="real",
path="",
source=self.source,
at=self.at,
)
}
_ =>
raise Type(
want="integer",
got=self.kind(),
path="",
source=self.source,
at=self.at,
)
}
}
///|
/// The number as a real.
pub fn Node::real(self : Node) -> Double raise Refused {
match self.value {
Real(x) => x
Int(n) => n.to_double()
_ =>
raise Type(
want="real",
got=self.kind(),
path="",
source=self.source,
at=self.at,
)
}
}
///|
/// The boolean.
pub fn Node::bool(self : Node) -> Bool raise Refused {
match self.value {
Bool(b) => b
_ =>
raise Type(
want="boolean",
got=self.kind(),
path="",
source=self.source,
at=self.at,
)
}
}
///|
/// The members of a list.
pub fn Node::list(self : Node) -> Array[Node] raise Refused {
match self.value {
List(items) => items
_ =>
raise Type(
want="list",
got=self.kind(),
path="",
source=self.source,
at=self.at,
)
}
}
///|
/// The members of a mapping.
pub fn Node::table(self : Node) -> Table raise Refused {
match self.value {
Map(table) => table
_ =>
raise Type(
want="mapping",
got=self.kind(),
path="",
source=self.source,
at=self.at,
)
}
}
///|
/// Whether this is a mapping.
pub fn Node::is_map(self : Node) -> Bool {
match self.value {
Map(_) => true
_ => false
}
}
///|
/// Whether this is a list.
pub fn Node::is_list(self : Node) -> Bool {
match self.value {
List(_) => true
_ => false
}
}
///|
/// A mapping node built from pairs, in the order given.
pub fn Node::map(
pairs : Array[(String, Node)],
source : String,
at? : At,
) -> Node {
let table = Table::new()
for pair in pairs {
table.set(pair.0, pair.1)
}
{ value: Map(table), source, at: at.unwrap_or(start), }
}
///|
/// A list node.
pub fn Node::seq(items : Array[Node], source : String, at? : At) -> Node {
{ value: List(items), source, at: at.unwrap_or(start), }
}
///|
/// A text node.
pub fn Node::str(text : String, source : String, at? : At) -> Node {
{ value: Text(text), source, at: at.unwrap_or(start), }
}
///|
/// A real-number node.
pub fn Node::of_real(value : Double, source : String, at? : At) -> Node {
{ value: Real(value), source, at: at.unwrap_or(start), }
}
///|
/// A boolean node.
pub fn Node::flag(flag : Bool, source : String, at? : At) -> Node {
{ value: Bool(flag), source, at: at.unwrap_or(start), }
}
///|
/// An integer node.
pub fn Node::num(n : Int64, source : String, at? : At) -> Node {
{ value: Int(n), source, at: at.unwrap_or(start), }
}
///|
/// A deep copy: the tree a caller gets back is theirs to change without
/// disturbing the one it came from.
pub fn Node::copy(self : Node) -> Node {
let value = match self.value {
List(items) => List(items.map(fn(item) { item.copy() }))
Map(table) => {
let out = Table::new()
for entry in table.entries {
out.set(entry.0, entry.1.copy())
}
Map(out)
}
other => other
}
{ value, source: self.source, at: self.at, }
}
///|
pub extend Table with Eq::{equal, not_equal}
///|
pub extend Table with Debug::{to_repr}
///|
pub extend Value with Eq::{equal, not_equal}
///|
pub extend Value with Debug::{to_repr}
///|
pub extend Node with Eq::{equal, not_equal}
///|
pub extend Node with Debug::{to_repr}