beck_core/
fields.rs

1//! Where a record literal's fields go, decided once instead of once per record.
2//!
3//! A record's fields are held in one order — by name — because that order is the `Map`'s
4//! iteration, the state digest and the patch stream a replay has to reproduce bit for bit
5//! ([`54`](../../../../../docs/54-ordering.md)). A record *literal* is written in some other
6//! order, usually the declaration's, so building one meant sorting three to eight names on every
7//! construction.
8//!
9//! The names are in the source, so the answer is the same every time the literal runs. This pass
10//! computes it once and writes it on the node: [`crate::core::Core::order`] holds, for each
11//! position of the finished record, which written field belongs there — four bits each, so a
12//! literal of up to [`MAX_ORDERED`] fields carries its whole layout in a `u32` that fits in
13//! padding `Core` already had.
14//!
15//! Evaluation order is **not** what changes. A field expression can `raise`, so the values are
16//! still computed in the order they are written; what the permutation removes is the comparing
17//! and moving afterwards.
18//!
19//! Like [`crate::frames`], being unable to answer is safe: [`UNORDERED`] means "sort at run
20//! time", and it is what a literal with a repeated field name, one with more fields than fit, and
21//! every program built by something that never runs this pass all carry.
22
23use crate::core::{Core, CoreKind};
24
25/// No layout on this node: whoever builds the record sorts it, as it did before this pass existed.
26///
27/// It cannot collide with a real layout. Every nibble of one is a written field's index, which is
28/// below [`MAX_ORDERED`], so no layout has `0xf` anywhere and this one is `0xf` everywhere.
29pub const UNORDERED: u32 = u32::MAX;
30
31/// How many fields fit in the packed layout: eight, at four bits each.
32///
33/// Nothing in this tree declares a record wider than that, and one that did would sort at run time
34/// rather than fail — which is why this is a constant here and not a limit in the language.
35pub const MAX_ORDERED: usize = 8;
36
37/// Put `items` — a record literal's fields, in the order they are written — into the order the
38/// record holds them in.
39///
40/// **One allocation, and it is the caller's.** That is the whole design constraint: the vector of
41/// evaluated fields already exists, so this permutes it where it lies rather than selecting out of
42/// it into a second one. Building the answer into a fresh vector was tried and measured, and it
43/// cost more than the sort it replaced — an allocation is dearer than an insertion sort over four
44/// names ([`78`](../../../../../docs/78-a-record-is-a-permutation-report.md) §78.3).
45///
46/// The permutation is followed in its cycles, which needs somewhere to record what has already
47/// been placed; here that is the packed layout itself, copied into a local and rewritten as it
48/// goes. So the bookkeeping is a `u32` in a register and the whole of this touches no memory but
49/// the elements it moves.
50///
51/// The caller has established that the node carries a layout ([`UNORDERED`] does not) and that the
52/// record is no wider than [`MAX_ORDERED`].
53pub fn place<T>(items: &mut [T], order: u32) {
54    let mut dest = order;
55    for i in 0..items.len() {
56        loop {
57            let j = nibble(dest, i);
58            if j == i {
59                break;
60            }
61            items.swap(i, j);
62            dest = swap_nibbles(dest, i, j);
63        }
64    }
65}
66
67/// Where the field written at position `at` belongs in the finished record.
68#[inline]
69fn nibble(order: u32, at: usize) -> usize {
70    ((order >> (4 * at)) & 0xf) as usize
71}
72
73#[inline]
74fn swap_nibbles(order: u32, i: usize, j: usize) -> u32 {
75    let (a, b) = (nibble(order, i) as u32, nibble(order, j) as u32);
76    let cleared = order & !(0xf << (4 * i)) & !(0xf << (4 * j));
77    cleared | (b << (4 * i)) | (a << (4 * j))
78}
79
80/// Give every record literal in the program its layout.
81pub fn order_program(program: &mut crate::check::Program) {
82    for def in program.defs.values_mut() {
83        order(&mut def.body);
84    }
85    for test in program.tests.iter_mut() {
86        for c in test.cores_mut() {
87            order(c);
88        }
89    }
90}
91
92/// The same for one expression, and everything under it.
93pub fn order(c: &mut Core) {
94    order_here(c);
95    for child in crate::core::children_mut(c) {
96        order(child);
97    }
98}
99
100/// The layout of this node alone, for a record synthesised after the pass has run — the splitter's
101/// fused state is the one that is.
102pub fn order_here(c: &mut Core) {
103    if let CoreKind::Make { fields, .. } = &c.kind {
104        c.order = layout(fields.iter().map(|(name, _)| name.as_ref()));
105    }
106}
107
108/// The packed layout for these field names, or [`UNORDERED`] when there is not one to give.
109fn layout<'a>(names: impl Iterator<Item = &'a str>) -> u32 {
110    let names: Vec<&str> = names.collect();
111    if names.is_empty() || names.len() > MAX_ORDERED {
112        return UNORDERED;
113    }
114    let mut by_name: Vec<usize> = (0..names.len()).collect();
115    by_name.sort_unstable_by(|&i, &j| crate::core::cmp_name(names[i], names[j]));
116    // A repeated name has no layout: two written fields would want one position, and which of them
117    // wins is a question the run-time sort answers today and this pass must not answer differently.
118    if by_name
119        .windows(2)
120        .any(|w| crate::core::cmp_name(names[w[0]], names[w[1]]) != std::cmp::Ordering::Less)
121    {
122        return UNORDERED;
123    }
124    let mut order = 0u32;
125    for (at, &written) in by_name.iter().enumerate() {
126        order |= (at as u32) << (4 * written);
127    }
128    order
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::core::{Const, VarId};
135    use crate::ty::Ty;
136    use beck_diag::Span;
137    use std::sync::Arc;
138
139    fn make(names: &[&str]) -> Core {
140        Core::new(
141            CoreKind::Make {
142                ty: "R".into(),
143                variant: None,
144                fields: names
145                    .iter()
146                    .map(|n| {
147                        (
148                            Arc::from(*n),
149                            Core::new(CoreKind::Const(Const::Int(0)), Ty::int(), Span::NONE),
150                        )
151                    })
152                    .collect(),
153            },
154            Ty::int(),
155            Span::NONE,
156        )
157    }
158
159    /// Apply a node's layout to its own field names, which is what the evaluator does to their
160    /// values.
161    fn placed(c: &Core, names: &[&str]) -> Vec<String> {
162        let mut items: Vec<String> = names.iter().map(|s| s.to_string()).collect();
163        place(&mut items, c.order);
164        items
165    }
166
167    #[test]
168    fn a_literal_written_in_order_is_the_identity() {
169        let mut c = make(&["a", "b", "c"]);
170        order(&mut c);
171        assert_eq!(placed(&c, &["a", "b", "c"]), ["a", "b", "c"]);
172    }
173
174    /// `Ball(x=…, y=…, x_vel=…, y_vel=…)` — declaration order, which is not name order.
175    #[test]
176    fn a_literal_written_in_declaration_order_permutes() {
177        let mut c = make(&["x", "y", "x_vel", "y_vel"]);
178        order(&mut c);
179        assert_eq!(
180            placed(&c, &["x", "y", "x_vel", "y_vel"]),
181            ["x", "x_vel", "y", "y_vel"]
182        );
183    }
184
185    /// Every permutation of eight names, placed — because a cycle-following permutation is exactly
186    /// the kind of code that is right for the shapes somebody thought to write down and wrong for
187    /// one of the 40,320 they did not.
188    #[test]
189    fn every_permutation_of_a_full_record_places_correctly() {
190        fn perms(items: &mut Vec<usize>, k: usize, out: &mut Vec<Vec<usize>>) {
191            if k == items.len() {
192                out.push(items.clone());
193                return;
194            }
195            for i in k..items.len() {
196                items.swap(k, i);
197                perms(items, k + 1, out);
198                items.swap(k, i);
199            }
200        }
201        let names = ["a", "b", "c", "d", "e", "f", "g", "h"];
202        let mut all = Vec::new();
203        perms(&mut (0..names.len()).collect(), 0, &mut all);
204        assert_eq!(all.len(), 40_320);
205        for p in all {
206            let written: Vec<&str> = p.iter().map(|&i| names[i]).collect();
207            let mut c = make(&written);
208            order(&mut c);
209            assert_eq!(placed(&c, &written), names, "written as {written:?}");
210        }
211    }
212
213    #[test]
214    fn a_repeated_name_has_no_layout() {
215        let mut c = make(&["a", "b", "a"]);
216        order(&mut c);
217        assert_eq!(c.order, UNORDERED);
218    }
219
220    #[test]
221    fn a_record_wider_than_the_packing_has_no_layout() {
222        let wide: Vec<String> = (0..MAX_ORDERED + 1).map(|i| format!("f{i}")).collect();
223        let refs: Vec<&str> = wide.iter().map(|s| s.as_str()).collect();
224        let mut c = make(&refs);
225        order(&mut c);
226        assert_eq!(c.order, UNORDERED);
227    }
228
229    /// The point of the sentinel: it is not reachable as an answer, so "no layout" and "this
230    /// layout" cannot be confused.
231    #[test]
232    fn a_layout_is_never_the_sentinel() {
233        for n in 1..=MAX_ORDERED {
234            let names: Vec<String> = (0..n).map(|i| format!("f{}", MAX_ORDERED - i)).collect();
235            let refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
236            let mut c = make(&refs);
237            order(&mut c);
238            assert_ne!(c.order, UNORDERED);
239        }
240    }
241
242    /// A literal nested inside a lambda gets its layout too — the walk is what this checks, since
243    /// a lambda's body is behind an `Arc` and is the one child that needs unsharing.
244    #[test]
245    fn a_literal_inside_a_lambda_is_reached() {
246        let inner = make(&["b", "a"]);
247        let mut lam = Core::new(
248            CoreKind::Lam {
249                params: Arc::from(vec![0 as VarId]),
250                body: Arc::new(inner),
251            },
252            Ty::int(),
253            Span::NONE,
254        );
255        order(&mut lam);
256        let CoreKind::Lam { body, .. } = &lam.kind else {
257            unreachable!()
258        };
259        assert_eq!(placed(body, &["b", "a"]), ["a", "b"]);
260    }
261}