rucc-sema 0.10.21

Type checking, conversions, initialization, constant evaluation, and the typed AST.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
//! The atomic accesses and the barrier: `__atomic_load_n` and its neighbours.
//!
//! Design: `spec/13-gnu-compat.md` section 13.5, and tamnd/rucc#311.
//!
//! Forty two names here, out of a family of forty three. `__atomic_load_n` reads an object, and
//! `__atomic_store_n` writes one, both without tearing and both with an ordering that says what
//! may be moved across them. `__atomic_load` and `__atomic_store` are the same two for an object
//! too big to come back in a register, so the value travels through a second pointer rather than
//! being taken or answered. `__atomic_thread_fence` is that ordering with no access attached, and
//! `__sync_synchronize` is the same barrier at sequential consistency under the older family's
//! spelling. `__atomic_always_lock_free` and `__atomic_is_lock_free` are not operations at all:
//! they ask whether an object of a given size is one the machine handles without a lock, and both
//! are constants worked out here.
//!
//! Four compare and exchange. `__atomic_compare_exchange_n` and `__atomic_compare_exchange` are the
//! C11 family's, and `__sync_bool_compare_and_swap` and `__sync_val_compare_and_swap` are the older
//! one's. All four are the same instruction and differ in what they answer and in whether the value
//! expected arrived by pointer or by value.
//!
//! Twenty eight read, do something to what they read, and write it back. `__atomic_exchange_n` puts
//! a value there and answers what was there, and `__atomic_exchange` does it through pointers for
//! the reason the pair above do. Each of the six operations comes in four spellings, two per family,
//! and the two of a pair differ in whether they answer the value before or the value after.
//! `__sync_lock_test_and_set` and `__sync_lock_release` are the two halves of a lock, which is an
//! exchange and a store of a zero at the two orderings a lock needs.
//!
//! Two are that same lock over a byte. `__atomic_test_and_set` and `__atomic_clear` are what an
//! `atomic_flag` is made of, and what makes them a pair of their own rather than the lock names
//! again is the object: one byte whatever the pointer they were handed points at, holding a value
//! the implementation picks rather than one the program hands over.
//!
//! SQLite is why the first four and not some other four. Its `AtomicLoad` and `AtomicStore` macros
//! are `__atomic_load_n` and `__atomic_store_n` at relaxed ordering, it calls `__sync_synchronize`
//! directly twice, and those three names are the whole of what an amalgamation build asks for. The
//! two questions are here because glibc's headers ask them and because the answer is arithmetic
//! over two numbers, so the cost of having them is a page of reasons and eight lines of code. The
//! compare and exchange is here because it is the instruction every other atomic on this machine is
//! built out of, and the twenty seven are here because glibc and the kernel are written out of them:
//! a reference count is `__atomic_fetch_add`, a spin lock is the pair of lock names, and a flag set
//! in a word of them is `__atomic_fetch_or`.
//!
//! # Why they are nodes
//!
//! An ordering is not an argument. It is something the IR says about an access, the way an
//! alignment is, and there is no function anywhere that a call could reach: no object file defines
//! `__atomic_load_n`, and if one did, a call to it would be a call and a call is exactly the thing
//! an ordering has to be able to constrain. So the call becomes a node, the same way the byte swaps
//! and the overflow checks do, and the walk to the IR builds an access with the ordering on it.
//!
//! # The ordering has to be a constant
//!
//! C says the argument is an `int` and does not say it is constant, so a program may write one the
//! compiler cannot fold. gcc treats that as sequential consistency, which is the only safe reading:
//! the ordering has to be decided before the program runs, and the strongest one is right whatever
//! the program would have passed. This does the same.
//!
//! A number that names no ordering, or an ordering the operation cannot have, is W0333 and is then
//! taken as sequential consistency for the same reason. gcc warns rather than refusing here, and a
//! refusal would break the macro-heavy code this family appears in, where an argument is often a
//! macro that expands differently per platform.
//!
//! # What is not here
//!
//! `__atomic_signal_fence`, which orders against a signal handler on the same thread and so has to
//! constrain the compiler while emitting no instruction at all. The IR's `fence` is a machine
//! barrier, so spelling a signal fence as one would be correct and would cost an `mfence` that
//! nothing needs. It waits for a barrier that says what it means, and it is the whole of what is
//! left.

use rucc_ast::UnaryOp;
use rucc_diag::{Diagnostic, Span};
use rucc_types::{IntKind, layout, pointee};

use crate::check::Checker;
use crate::expr::{AtomicOp, Category, Expr, ExprId, ExprKind, Ordering, Rmw};
use crate::tast::Const;

/// The names that reach this through the type generic table, and what each one is.
///
/// `__sync_synchronize` is not here because it carries a signature, so it is checked as an ordinary
/// call and is answered by [`Checker::sync_builtin_value`] instead.
const FAMILY: &[(&str, AtomicOp)] = &[
    ("__atomic_load_n", AtomicOp::Load),
    ("__atomic_load", AtomicOp::LoadInto),
    ("__atomic_store_n", AtomicOp::Store),
    ("__atomic_store", AtomicOp::Store),
    ("__atomic_thread_fence", AtomicOp::Fence),
    ("__atomic_compare_exchange_n", AtomicOp::CompareExchange),
    ("__atomic_compare_exchange", AtomicOp::CompareExchange),
    ("__sync_bool_compare_and_swap", AtomicOp::SwapBool),
    ("__sync_val_compare_and_swap", AtomicOp::SwapValue),
    ("__atomic_exchange_n", AtomicOp::Exchange),
    ("__atomic_exchange", AtomicOp::ExchangeInto),
    ("__atomic_test_and_set", AtomicOp::TestAndSet),
    ("__atomic_clear", AtomicOp::Store),
    ("__atomic_fetch_add", AtomicOp::Fetch(Rmw::Add)),
    ("__atomic_fetch_sub", AtomicOp::Fetch(Rmw::Sub)),
    ("__atomic_add_fetch", AtomicOp::Update(Rmw::Add)),
    ("__atomic_sub_fetch", AtomicOp::Update(Rmw::Sub)),
    ("__sync_fetch_and_add", AtomicOp::Fetch(Rmw::Add)),
    ("__sync_fetch_and_sub", AtomicOp::Fetch(Rmw::Sub)),
    ("__sync_add_and_fetch", AtomicOp::Update(Rmw::Add)),
    ("__sync_sub_and_fetch", AtomicOp::Update(Rmw::Sub)),
    ("__sync_lock_test_and_set", AtomicOp::Exchange),
    ("__sync_lock_release", AtomicOp::Store),
    ("__atomic_fetch_and", AtomicOp::Fetch(Rmw::And)),
    ("__atomic_fetch_nand", AtomicOp::Fetch(Rmw::Nand)),
    ("__atomic_fetch_or", AtomicOp::Fetch(Rmw::Or)),
    ("__atomic_fetch_xor", AtomicOp::Fetch(Rmw::Xor)),
    ("__atomic_and_fetch", AtomicOp::Update(Rmw::And)),
    ("__atomic_nand_fetch", AtomicOp::Update(Rmw::Nand)),
    ("__atomic_or_fetch", AtomicOp::Update(Rmw::Or)),
    ("__atomic_xor_fetch", AtomicOp::Update(Rmw::Xor)),
    ("__sync_fetch_and_and", AtomicOp::Fetch(Rmw::And)),
    ("__sync_fetch_and_nand", AtomicOp::Fetch(Rmw::Nand)),
    ("__sync_fetch_and_or", AtomicOp::Fetch(Rmw::Or)),
    ("__sync_fetch_and_xor", AtomicOp::Fetch(Rmw::Xor)),
    ("__sync_and_and_fetch", AtomicOp::Update(Rmw::And)),
    ("__sync_nand_and_fetch", AtomicOp::Update(Rmw::Nand)),
    ("__sync_or_and_fetch", AtomicOp::Update(Rmw::Or)),
    ("__sync_xor_and_fetch", AtomicOp::Update(Rmw::Xor)),
];

/// The two names of the older family that are the two halves of a lock rather than a full barrier.
///
/// Everything else spelled `__sync_` orders everything against everything, and these two do not,
/// which is what gcc documents them as and is the whole reason they are spelled apart from
/// `__sync_lock_test_and_set`'s neighbours. Taking a lock has to keep what comes after it from
/// moving in front, and releasing one has to keep what came before it from moving out behind, and
/// neither has anything to say about the other direction.
const LOCK_TEST_AND_SET: &str = "__sync_lock_test_and_set";
const LOCK_RELEASE: &str = "__sync_lock_release";

/// The one of the two C11 compare and exchange names whose value to put there arrives by pointer.
///
/// The `_n` in the other one is the family's own mark for the form that takes a value, and the form
/// without it exists for an object too big to pass in a register. Both are here, and the difference
/// between them is one read, which is done where the name is still known so that everything below
/// sees the same shape.
const THROUGH_POINTER: &str = "__atomic_compare_exchange";

/// The write of the C11 family whose value arrives by pointer, for the reason above.
///
/// The read and the exchange of the same shape are not here, because each of them is a shape of its
/// own in [`AtomicOp`] rather than a spelling of one: both answer nothing and write what they read
/// through a pointer, and that is a difference in what the walk to the IR does rather than in one
/// argument. A store hands over a value either way and has nowhere to write back to, so the two
/// spellings really are one operation and one read apart.
const STORE_THROUGH_POINTER: &str = "__atomic_store";

/// The two names whose object is one byte whatever the pointer they were handed points at.
///
/// gcc takes any pointer here, including a `void *`, and writes one byte through it. That is the
/// standard's reading rather than a liberty: the object is `atomic_flag`, which is not a value the
/// program reads or writes by any other means, so the type the pointer was written with says
/// nothing about the access and the width is the implementation's to fix.
///
/// So is the value that means set, and [`SET`] is the one this picks.
const CLEAR: &str = "__atomic_clear";

/// The byte `__atomic_test_and_set` puts in, and so the byte `__atomic_clear` takes back out.
///
/// Written once because the pair only works while the two agree: what the first answers is whether
/// the flag was already held, and what makes that answer true is finding this byte there. A program
/// cannot tell which byte it is, since there is no way to read an `atomic_flag` except through these
/// two names.
const SET: i128 = 1;

/// The one name of the older family that is not type generic.
const SYNCHRONIZE: &str = "__sync_synchronize";

/// The two names that ask about the target rather than about an object.
///
/// They are one answer here, which is a decision rather than an oversight and is written out where
/// [`Checker::lock_free_builtin_value`] answers them.
const LOCK_FREE: &[&str] = &["__atomic_always_lock_free", "__atomic_is_lock_free"];

/// The numbers `<stdatomic.h>` and gcc's own headers give the orderings, in the order gcc gives
/// them.
///
/// `memory_order_consume` is the second, and it becomes [`Ordering::Acquire`] here. Every compiler
/// in use gives the two the same code, and a spelling of consume that means acquire would be a name
/// whose only effect is to make a reader think it was implemented.
const NUMBERED: &[Ordering] = &[
    Ordering::Relaxed,
    Ordering::Acquire,
    Ordering::Acquire,
    Ordering::Release,
    Ordering::AcqRel,
    Ordering::SeqCst,
];

/// Which shape a type generic name is, if it is one of these.
pub(in crate::check) fn shape(spelled: &str) -> Option<AtomicOp> {
    FAMILY.iter().find(|&&(name, _)| name == spelled).map(|&(_, op)| op)
}

/// Whether an operation of this shape can carry this ordering.
///
/// A load cannot release, because it wrote nothing for anybody to see, and a store cannot acquire,
/// because it read nothing to synchronise with. A barrier can be any of them, including relaxed,
/// which orders nothing and is what a program writes when the ordering is a macro that came out
/// relaxed on this platform.
///
/// A compare and exchange can be any of them, and so can a read modify write, because both read
/// and write and so have something to say about both directions. The ordering that holds when a
/// compare and exchange exchanged nothing is checked as a load's rather than here, since what the
/// operation did in that case is read the object and write nothing, which is a load.
fn allowed(op: AtomicOp, order: Ordering) -> bool {
    match op {
        AtomicOp::Load | AtomicOp::LoadInto => {
            matches!(order, Ordering::Relaxed | Ordering::Acquire | Ordering::SeqCst)
        }
        AtomicOp::Store => {
            matches!(order, Ordering::Relaxed | Ordering::Release | Ordering::SeqCst)
        }
        AtomicOp::Fence
        | AtomicOp::CompareExchange
        | AtomicOp::SwapBool
        | AtomicOp::SwapValue
        | AtomicOp::Exchange
        | AtomicOp::ExchangeInto
        | AtomicOp::TestAndSet
        | AtomicOp::Fetch(_)
        | AtomicOp::Update(_) => true,
    }
}

impl Checker<'_> {
    /// The node one of the type generic atomics becomes, once its arguments have been checked.
    ///
    /// The arguments arrive as values in the types they were written with. The ones that have to be
    /// converted are the values going into the object, which become the type of the object they are
    /// going into, because that is the width of the access and a store of a `char` through an
    /// `int *` is a four byte write.
    ///
    /// # What a compare and exchange drops
    ///
    /// Two of the C11 form's six arguments are checked and then thrown away, and both are thrown
    /// away because of what this machine is rather than because nobody got to them.
    ///
    /// The `weak` argument says the operation may fail when the object did hold the expected value,
    /// which lets a machine whose compare and exchange is a pair of linked instructions leave the
    /// retry loop to the caller. x86-64's is one instruction and never fails that way, so a weak
    /// compare and exchange and a strong one are the same instruction here and the argument decides
    /// nothing. gcc requires it to be a constant and this does not read it at all.
    ///
    /// The failure ordering says how strongly the operation is ordered when nothing was exchanged.
    /// It is checked, because a program that writes a release there has written something that is
    /// wrong everywhere, and then dropped, because the instruction this becomes is a locked one and
    /// a locked instruction on x86-64 is a full barrier whichever ordering was asked for.
    pub(in crate::check) fn atomic_builtin(
        &mut self,
        op: AtomicOp,
        spelled: &str,
        args: &[ExprId],
        span: Span,
    ) -> ExprId {
        let Some(order) = self.order_of(op, args, spelled) else { return self.poison(span) };

        let operands = match op {
            AtomicOp::Fence => Vec::new(),
            AtomicOp::Load => vec![args[0]],
            // The object and the place what was read goes into. Neither is converted, because
            // nothing is answered here for anything to convert: the walk to the IR reads at the
            // width of the object and writes that value straight back out through the second
            // pointer, which checking has already made a pointer to that same type.
            AtomicOp::LoadInto => vec![args[0], args[1]],
            // The value goes in as the type of the object, which is the width of the access: a
            // store of a `char` through an `int *` writes four bytes. The byte the flag names touch
            // is the exception, and it is why the object here is worked out from the name.
            AtomicOp::Store | AtomicOp::Exchange | AtomicOp::Fetch(_) | AtomicOp::Update(_) => {
                let target = self.object(spelled, args[0]);
                vec![args[0], self.written(spelled, args, target, span)]
            }
            // The object, the value to put there, and the place what was there goes into. The
            // value is read here for the same reason a store's is, and the place is left as it
            // stands for the same reason a read into one is.
            AtomicOp::ExchangeInto => {
                let target = self.accessed(args[0]);
                let value = self.value_at(args[1], target, span);
                vec![args[0], value, args[2]]
            }
            // The byte, and the value that goes into it. See [`CLEAR`] for why one byte and
            // [`SET`] for why that value.
            AtomicOp::TestAndSet => {
                let byte = self.types.int(IntKind::UChar);
                vec![args[0], self.constant(Const::Int(SET), byte, span)]
            }
            // The object, the place the value expected is, and the value to put there. The second
            // is a pointer in the C11 pair and a value in the older one, and it stays as written
            // rather than being made the same in both, because what is done with it differs: one
            // pair writes back through it and the other has nowhere to write back to.
            AtomicOp::CompareExchange => {
                let target = self.accessed(args[0]);
                let desired = if spelled == THROUGH_POINTER {
                    self.value_at(args[2], target, span)
                } else {
                    self.conv().to_type(args[2], target)
                };
                vec![args[0], args[1], desired]
            }
            AtomicOp::SwapBool | AtomicOp::SwapValue => {
                let target = self.accessed(args[0]);
                let expected = self.conv().to_type(args[1], target);
                let desired = self.conv().to_type(args[2], target);
                vec![args[0], expected, desired]
            }
        };
        let ty = match op {
            AtomicOp::Load
            | AtomicOp::SwapValue
            | AtomicOp::Exchange
            | AtomicOp::Fetch(_)
            | AtomicOp::Update(_) => self.accessed(args[0]),
            AtomicOp::CompareExchange | AtomicOp::SwapBool | AtomicOp::TestAndSet => {
                self.types.boolean()
            }
            AtomicOp::Store | AtomicOp::LoadInto | AtomicOp::ExchangeInto | AtomicOp::Fence => {
                self.types.void()
            }
        };
        let args = self.tast.add_expr_refs(&operands);
        self.tast.expr(Expr::new(ExprKind::Atomic { op, order, args }, ty, Category::Rvalue), span)
    }

    /// Which ordering the call asked for, out of however many orderings its name carries.
    ///
    /// One for the accesses, the barrier and the read modify writes of the C11 family, where it is
    /// the last argument, which is the shape that family has: the object comes first, whatever it is
    /// being handed comes next, and how strongly it is ordered comes last. Two for a compare and
    /// exchange, where the second is the one that holds when nothing was exchanged. None at all for
    /// the older family, which has no argument to say so with, and which of the two families a name
    /// is in is read off the spelling because that is exactly what it is.
    ///
    /// Nothing at all comes back when there is no argument where one was expected, which is a call
    /// that has already been complained about for its argument count.
    fn order_of(&mut self, op: AtomicOp, args: &[ExprId], spelled: &str) -> Option<Ordering> {
        // The trailing arguments of a `__sync_*` call are the variables it promises to protect, and
        // it protects them by being a full barrier, so there is nothing to read and nothing that
        // could have been written. The two halves of a lock are the exception and are weaker, which
        // is a thing gcc documents about them rather than a thing this works out.
        if spelled.starts_with("__sync_") {
            return Some(match spelled {
                LOCK_TEST_AND_SET => Ordering::Acquire,
                LOCK_RELEASE => Ordering::Release,
                _ => Ordering::SeqCst,
            });
        }
        if op == AtomicOp::CompareExchange {
            let [.., success, failure] = args else { return None };
            // Checked as a load's, because what the operation did when it exchanged nothing is read
            // the object and write nothing, which is a load. The answer is dropped: see above.
            let _ = self.ordering(AtomicOp::Load, *failure, spelled);
            return Some(self.ordering(op, *success, spelled));
        }
        let &written = args.last()?;
        Some(self.ordering(op, written, spelled))
    }

    /// The object a write under this name touches, which is what the pointer points at everywhere
    /// but one.
    ///
    /// `__atomic_clear` is the one, and its object is a byte however the pointer was written. See
    /// [`CLEAR`] for why that is the standard's reading rather than a shortcut.
    fn object(&mut self, spelled: &str, pointer: ExprId) -> rucc_types::TypeId {
        if spelled == CLEAR {
            return self.types.int(IntKind::UChar);
        }
        self.accessed(pointer)
    }

    /// The value going into the object, as the type of the object.
    ///
    /// Two of the four writes hand no value over. `__sync_lock_release` puts a zero there, which is
    /// how a lock is given back whatever the object it is held in, and everything after its first
    /// argument is the list of variables it promises to protect rather than anything to write.
    /// `__atomic_clear` puts back the byte `__atomic_test_and_set` treats as free, which is a zero
    /// because [`SET`] is a one.
    ///
    /// The zero is written as an `int` and then converted rather than made in the object's type
    /// directly, so that an object that is a pointer gets the null pointer and one that is a
    /// `double` gets a floating zero, both of which are what that conversion is for.
    ///
    /// The one write whose value arrives by pointer is read here, where the name is still known, so
    /// that both spellings of a store are the same shape by the time anything below sees them.
    fn written(
        &mut self,
        spelled: &str,
        args: &[ExprId],
        target: rucc_types::TypeId,
        span: Span,
    ) -> ExprId {
        let value = match spelled {
            LOCK_RELEASE | CLEAR => {
                let int = self.types.int(IntKind::Int);
                self.constant(Const::Int(0), int, span)
            }
            STORE_THROUGH_POINTER => self.value_at(args[1], target, span),
            _ => args[1],
        };
        self.conv().to_type(value, target)
    }

    /// The value at the end of a pointer the caller handed over, as an rvalue of the object's type.
    ///
    /// The argument has already been checked to be a pointer to the object type, by `argument_fits`
    /// in `check/builtin/generic.rs`, so the read is written here rather than going back through
    /// the checking of a `*` somebody typed.
    fn value_at(&mut self, pointer: ExprId, target: rucc_types::TypeId, span: Span) -> ExprId {
        let node = ExprKind::Unary { op: UnaryOp::Deref, operand: pointer };
        let read = self.tast.expr(Expr::new(node, target, Category::Lvalue), span);
        self.value(read)
    }

    /// `__sync_synchronize()`, which is a full barrier and takes nothing.
    ///
    /// Answers nothing for every other call in the program, so the test that costs a byte goes
    /// first. It is answered here rather than beside the three above because it has a signature in
    /// the table and so is checked against a prototype like any other call, which is the older
    /// family's one member that could be.
    pub(in crate::check) fn sync_builtin_value(
        &mut self,
        function: Option<rucc_base::Symbol>,
        span: Span,
    ) -> Option<ExprId> {
        let name = function?;
        let spelled = self.text(name);
        if !spelled.starts_with("__sync_") || spelled != SYNCHRONIZE {
            return None;
        }
        let ty = self.types.void();
        let args = self.tast.add_expr_refs(&[]);
        let kind = ExprKind::Atomic { op: AtomicOp::Fence, order: Ordering::SeqCst, args };
        Some(self.tast.expr(Expr::new(kind, ty, Category::Rvalue), span))
    }

    /// `__atomic_always_lock_free(size, p)` and `__atomic_is_lock_free(size, p)`, which are
    /// questions about the machine and answer as constants.
    ///
    /// Both take a size in bytes and a pointer that is there to say how the object is aligned, and
    /// both come back true when an object of that size and that alignment is one this compiler
    /// writes an instruction for rather than a call to a library. Which sizes those are is
    /// `lock_free_width` in `rucc_target::TargetInfo`, and it is eight bytes everywhere, so the
    /// answer here is that the size is one, two, four or eight and the object is aligned to at
    /// least its own size.
    ///
    /// # Why the two are one answer
    ///
    /// gcc separates them: the first has to be a constant and the second may become a call into
    /// libatomic, which decides at run time by looking at the address. There is no libatomic here
    /// and nothing to call, so a second answer would be a call to a function no object file
    /// defines. Folding both means a program that asks the second question gets the first
    /// question's answer, which is the stronger claim and so is never wrong where it says yes. The
    /// only thing a run time answer knows that this does not is what an address turned out to be
    /// aligned to, and nothing on this target does eight bytes atomically at one alignment and not
    /// at another, so there is no case where the second question has a better answer than this.
    ///
    /// # The size, and what a size that is not a constant means
    ///
    /// A size the compiler cannot fold answers no. It has to answer something, since the whole
    /// point of both names is that the answer is available before the program runs, and no is the
    /// answer that makes a program take the path that works whatever the size turns out to be. gcc
    /// refuses the first name outright in that case, and refusing here would break the header idiom
    /// these appear in, where the size is a macro that came out of some other platform's header.
    ///
    /// # The pointer
    ///
    /// A null pointer means the object has whatever alignment its type would naturally have, which
    /// is what gcc documents and is what every use in a header passes. Anything else is read for
    /// the type it points at, through the conversion to `const void *` that the prototype put
    /// there, since reading the argument where it stands would be asking a `void` how it is
    /// aligned. A pointer to something with no layout, which is a `void *` or an incomplete type,
    /// says nothing and is treated as the null pointer is.
    ///
    /// The alignment comes from the type and not from an analysis of the address, and gcc's comes
    /// from the address. So `__atomic_always_lock_free(4, &p->v)` where `v` is an `int` in a packed
    /// structure is yes here and no there: this sees an `int *` and gcc sees a field it laid out at
    /// an odd offset. The answer is not wrong on this machine, because the `lock` prefix works at
    /// any alignment on x86-64 and the operation really is lock free, and it would be wrong on a
    /// machine where it is not, so it is written down here rather than left for a target that has
    /// to care about it to discover.
    pub(in crate::check) fn lock_free_builtin_value(
        &mut self,
        function: Option<rucc_base::Symbol>,
        args: &[ExprId],
        span: Span,
    ) -> Option<ExprId> {
        let name = function?;
        let spelled = self.text(name);
        if !spelled.starts_with("__atomic_") || !LOCK_FREE.contains(&spelled) {
            return None;
        }
        let &[size, object] = args else { return None };
        let widest = u128::from(self.cx.target.lock_free_width / 8);
        let bytes = self.folded(size).and_then(|number| u128::try_from(number).ok());
        let free = bytes.is_some_and(|bytes| {
            bytes.is_power_of_two()
                && bytes <= widest
                && u128::from(self.aligned_to(object)) >= bytes
        });
        let boolean = self.types.boolean();
        Some(self.constant(Const::Int(i128::from(free)), boolean, span))
    }

    /// What that expression is as a number, or nothing if it is not one.
    ///
    /// The complaints folding made are dropped for the reason [`Checker::ordering`] drops them: a
    /// non constant argument is allowed in both places, and what folding says about one is that it
    /// is not a constant, which is not a complaint about this program.
    fn folded(&mut self, expr: ExprId) -> Option<i128> {
        if self.is_poisoned(expr) {
            return None;
        }
        let mut eval = self.eval();
        let folded = eval.constant(expr);
        let _ = eval.finish();
        match folded {
            Ok(Const::Int(number)) => Some(number),
            _ => None,
        }
    }

    /// What the object this pointer points at is aligned to, in bytes.
    ///
    /// Where nothing was said, which is the null pointer and the pointer to something with no
    /// layout, the answer is as large as it can be, so that the alignment stops being part of the
    /// question and the size decides it alone.
    fn aligned_to(&mut self, object: ExprId) -> u64 {
        if self.conv().is_null_pointer_constant(object) {
            return u64::MAX;
        }
        let mut expr = object;
        // Through the conversion the prototype put there, which is what holds the type that was
        // written. The parameter is `const void *` and a `void` has no alignment, so reading the
        // argument where it stands would answer nothing for every call.
        while let ExprKind::Cast(inner) | ExprKind::Convert { operand: inner, .. } =
            self.tast[expr].kind
        {
            expr = inner;
        }
        let Some(target) = pointee(&self.types, self.tast[expr].ty) else { return u64::MAX };
        layout(&self.types, target, self.cx.target).map_or(u64::MAX, |it| it.align)
    }

    /// The type an access through this pointer touches, with the qualifiers off it.
    ///
    /// The argument has already been checked to be a pointer to something that is not `void`, by
    /// `object_type` in `check/builtin/generic.rs`, so the fallback here is unreachable in a program
    /// that got this far and is written rather than asserted because a poisoned argument can reach
    /// it and has already been complained about.
    fn accessed(&mut self, object: ExprId) -> rucc_types::TypeId {
        match pointee(&self.types, self.tast[object].ty) {
            Some(target) => self.plain(target),
            None => self.tast[object].ty,
        }
    }

    /// The ordering the source asked for, checked against what the operation can carry.
    ///
    /// Everything that is not an ordering this operation can have comes back as sequential
    /// consistency, which is stronger than anything the program could have meant and so is the one
    /// answer that cannot make a working program wrong.
    fn ordering(&mut self, op: AtomicOp, written: ExprId, spelled: &str) -> Ordering {
        if self.is_poisoned(written) {
            return Ordering::SeqCst;
        }
        let mut eval = self.eval();
        let folded = eval.constant(written);
        // The messages folding produced are dropped rather than reported. A non-constant argument
        // is allowed here, and what folding says about one is that it is not a constant, which is
        // not news to anybody and is not a complaint about this program.
        let _ = eval.finish();
        let at = self.tast.expr_span(written);
        let Ok(Const::Int(number)) = folded else {
            return Ordering::SeqCst;
        };
        let known = usize::try_from(number).ok().and_then(|index| NUMBERED.get(index).copied());
        let Some(order) = known.filter(|&order| allowed(op, order)) else {
            self.report(
                Diagnostic::warning(
                    format!(
                        "{number} is not a memory order '{spelled}' can be given, so this is \
                         ordered as if it were sequentially consistent"
                    ),
                    at,
                )
                .with_code("W0333"),
            );
            return Ordering::SeqCst;
        };
        order
    }
}

#[cfg(test)]
mod tests {
    use rucc_gnu::{Kind, Status};

    use super::*;

    /// Every type generic name has to be a row of the roster that carries no signature, or the
    /// ordinary call checking would answer for it before this ever sees it.
    #[test]
    fn the_generic_names_are_rows_of_the_table_that_carry_no_signature() {
        for &(name, _) in FAMILY {
            let Some(feature) = rucc_gnu::lookup(Kind::Builtin, name) else {
                panic!("{name} is answered here and is not in features.toml");
            };
            assert_eq!(feature.status, Status::Implemented, "{name}");
            assert!(feature.signature.is_empty(), "{name} has a signature and is type generic");
        }
    }

    /// The older family's one member with a signature, which is the opposite requirement.
    #[test]
    fn the_barrier_of_the_older_family_is_a_row_that_carries_one() {
        let feature = rucc_gnu::lookup(Kind::Builtin, SYNCHRONIZE).expect("a row of features.toml");
        assert_eq!(feature.status, Status::Implemented);
        assert!(!feature.signature.is_empty(), "it is checked against its prototype");
        assert!(feature.library.is_empty(), "it is not a call to anything");
    }

    /// The numbers are gcc's and glibc's, and getting one of them wrong would turn a release into
    /// an acquire without anything noticing, so they are written out rather than counted.
    #[test]
    fn the_numbers_are_the_ones_the_headers_use() {
        assert_eq!(NUMBERED[0], Ordering::Relaxed);
        assert_eq!(NUMBERED[2], Ordering::Acquire);
        assert_eq!(NUMBERED[3], Ordering::Release);
        assert_eq!(NUMBERED[4], Ordering::AcqRel);
        assert_eq!(NUMBERED[5], Ordering::SeqCst);
        assert_eq!(NUMBERED.len(), 6);
    }

    /// Consume is the one that is not itself, and it is worth its own test because the reason is a
    /// decision rather than a fact about the numbering.
    #[test]
    fn consume_is_read_as_acquire() {
        assert_eq!(NUMBERED[1], Ordering::Acquire);
    }

    /// A load cannot release and a store cannot acquire, and both of those are things a program
    /// reaches by passing a macro that came out of some other platform's header.
    #[test]
    fn an_operation_refuses_the_orderings_it_has_nothing_to_say_about() {
        assert!(!allowed(AtomicOp::Load, Ordering::Release));
        assert!(!allowed(AtomicOp::Load, Ordering::AcqRel));
        assert!(!allowed(AtomicOp::Store, Ordering::Acquire));
        assert!(!allowed(AtomicOp::Store, Ordering::AcqRel));
    }

    /// And what each of them can carry, including relaxed, which orders nothing and is what SQLite
    /// writes.
    #[test]
    fn every_operation_takes_the_orderings_it_means_something_for() {
        assert!(allowed(AtomicOp::Load, Ordering::Relaxed));
        assert!(allowed(AtomicOp::Load, Ordering::Acquire));
        assert!(allowed(AtomicOp::Load, Ordering::SeqCst));
        assert!(allowed(AtomicOp::Store, Ordering::Relaxed));
        assert!(allowed(AtomicOp::Store, Ordering::Release));
        assert!(allowed(AtomicOp::Store, Ordering::SeqCst));
        for &order in NUMBERED {
            assert!(allowed(AtomicOp::Fence, order), "a barrier takes {order:?}");
        }
    }

    /// The name whose desired value arrives through a pointer is spelled out in one place and used
    /// in another, so the two are checked against each other rather than against a reader.
    #[test]
    fn the_name_that_takes_its_desired_value_through_a_pointer_is_one_of_the_family() {
        assert_eq!(shape(THROUGH_POINTER), Some(AtomicOp::CompareExchange));
        assert_eq!(THROUGH_POINTER, "__atomic_compare_exchange");
        assert_ne!(
            THROUGH_POINTER, "__atomic_compare_exchange_n",
            "the suffixed one takes a value"
        );
    }

    /// An exchange reads and writes, so unlike the two accesses there is no ordering it has nothing
    /// to say about, and a program that hands one a release is not to be turned away.
    #[test]
    fn an_exchange_takes_every_ordering_there_is() {
        for op in [AtomicOp::CompareExchange, AtomicOp::SwapBool, AtomicOp::SwapValue] {
            for &order in NUMBERED {
                assert!(allowed(op, order), "an exchange takes {order:?}");
            }
        }
    }

    /// The two questions carry a prototype for the reason the barrier does, which is what gets them
    /// to the place they are answered.
    #[test]
    fn the_two_questions_are_rows_that_carry_a_signature() {
        for &name in LOCK_FREE {
            let feature = rucc_gnu::lookup(Kind::Builtin, name).expect("a row of features.toml");
            assert_eq!(feature.status, Status::Implemented, "{name}");
            assert!(!feature.signature.is_empty(), "{name} is checked against its prototype");
            assert!(feature.library.is_empty(), "{name} is not a call to anything");
            assert!(shape(name).is_none(), "{name} is not one of the type generic ones");
        }
    }

    /// A name outside the family asks for nothing, which is now the two that are not operations and
    /// the one that is the rest of tamnd/rucc#311.
    #[test]
    fn a_name_outside_the_family_asks_for_nothing() {
        assert_eq!(shape("__atomic_load_n"), Some(AtomicOp::Load));
        assert_eq!(shape("__atomic_store_n"), Some(AtomicOp::Store));
        assert_eq!(shape("__atomic_thread_fence"), Some(AtomicOp::Fence));
        assert_eq!(shape("__atomic_compare_exchange_n"), Some(AtomicOp::CompareExchange));
        assert_eq!(shape("__atomic_compare_exchange"), Some(AtomicOp::CompareExchange));
        assert_eq!(shape("__sync_bool_compare_and_swap"), Some(AtomicOp::SwapBool));
        assert_eq!(shape("__sync_val_compare_and_swap"), Some(AtomicOp::SwapValue));
        assert_eq!(shape("__atomic_exchange_n"), Some(AtomicOp::Exchange));
        assert_eq!(shape(LOCK_TEST_AND_SET), Some(AtomicOp::Exchange));
        assert_eq!(shape(LOCK_RELEASE), Some(AtomicOp::Store));
        assert_eq!(shape("__atomic_fetch_add"), Some(AtomicOp::Fetch(Rmw::Add)));
        assert_eq!(shape("__atomic_sub_fetch"), Some(AtomicOp::Update(Rmw::Sub)));
        assert_eq!(shape("__sync_fetch_and_sub"), Some(AtomicOp::Fetch(Rmw::Sub)));
        assert_eq!(shape("__sync_add_and_fetch"), Some(AtomicOp::Update(Rmw::Add)));
        assert_eq!(shape("__atomic_fetch_and"), Some(AtomicOp::Fetch(Rmw::And)));
        assert_eq!(shape("__atomic_nand_fetch"), Some(AtomicOp::Update(Rmw::Nand)));
        assert_eq!(shape("__sync_fetch_and_or"), Some(AtomicOp::Fetch(Rmw::Or)));
        assert_eq!(shape("__sync_xor_and_fetch"), Some(AtomicOp::Update(Rmw::Xor)));
        assert_eq!(shape("__atomic_load"), Some(AtomicOp::LoadInto));
        assert_eq!(shape(STORE_THROUGH_POINTER), Some(AtomicOp::Store));
        assert_eq!(shape("__atomic_exchange"), Some(AtomicOp::ExchangeInto));
        assert_eq!(shape("__atomic_test_and_set"), Some(AtomicOp::TestAndSet));
        assert_eq!(shape(CLEAR), Some(AtomicOp::Store));
        assert_eq!(shape("__atomic_signal_fence"), None);
        assert_eq!(shape(SYNCHRONIZE), None);
    }
}