openMSX
MSXCPU.cc
Go to the documentation of this file.
1#include "MSXCPU.hh"
2#include "MSXCPUInterface.hh"
3#include "MSXMotherBoard.hh"
4#include "Debugger.hh"
5#include "Scheduler.hh"
6#include "IntegerSetting.hh"
7#include "CPUCore.hh"
8#include "Z80.hh"
9#include "R800.hh"
10#include "TclObject.hh"
11#include "outer.hh"
12#include "ranges.hh"
13#include "serialize.hh"
14#include "unreachable.hh"
15#include "xrange.hh"
16#include <cassert>
17#include <memory>
18
19namespace openmsx {
20
22 : motherboard(motherboard_)
23 , traceSetting(
24 motherboard.getCommandController(), "cputrace",
25 "CPU tracing on/off", false, Setting::DONT_SAVE)
26 , diHaltCallback(
27 motherboard.getCommandController(), "di_halt_callback",
28 "Tcl proc called when the CPU executed a DI/HALT sequence")
29 , z80(std::make_unique<CPUCore<Z80TYPE>>(
30 motherboard, "z80", traceSetting,
31 diHaltCallback, EmuTime::zero()))
32 , r800(motherboard.isTurboR()
33 ? std::make_unique<CPUCore<R800TYPE>>(
34 motherboard, "r800", traceSetting,
35 diHaltCallback, EmuTime::zero())
36 : nullptr)
37 , timeInfo(motherboard.getMachineInfoCommand())
38 , z80FreqInfo(motherboard.getMachineInfoCommand(), "z80_freq", *z80)
39 , r800FreqInfo(r800
40 ? std::make_unique<CPUFreqInfoTopic>(
41 motherboard.getMachineInfoCommand(), "r800_freq", *r800)
42 : nullptr)
43 , debuggable(motherboard_)
44 , reference(EmuTime::zero())
45{
46 z80Active = true; // setActiveCPU(CPU_Z80);
47 newZ80Active = z80Active;
48
49 motherboard.getDebugger().setCPU(this);
50 motherboard.getScheduler().setCPU(this);
51 traceSetting.attach(*this);
52
53 z80->freqLocked.attach(*this);
54 z80->freqValue.attach(*this);
55 if (r800) {
56 r800->freqLocked.attach(*this);
57 r800->freqValue.attach(*this);
58 }
59 invalidateMemCacheSlot();
60}
61
63{
64 traceSetting.detach(*this);
65 z80->freqLocked.detach(*this);
66 z80->freqValue.detach(*this);
67 if (r800) {
68 r800->freqLocked.detach(*this);
69 r800->freqValue.detach(*this);
70 }
71 motherboard.getScheduler().setCPU(nullptr);
72 motherboard.getDebugger() .setCPU(nullptr);
73}
74
76{
77 interface = interface_;
78 z80 ->setInterface(interface);
79 if (r800) r800->setInterface(interface);
80}
81
82void MSXCPU::doReset(EmuTime::param time)
83{
84 z80 ->doReset(time);
85 if (r800) r800->doReset(time);
86
87 invalidateAllSlotsRWCache(0x0000, 0x10000);
88
89 reference = time;
90}
91
93{
94 if (cpu == CPU_R800) assert(r800);
95
96 bool tmp = cpu == CPU_Z80;
97 if (tmp != z80Active) {
99 newZ80Active = tmp;
100 }
101}
102
103void MSXCPU::setDRAMmode(bool dram)
104{
105 assert(r800);
106 r800->setDRAMmode(dram);
107}
108
109void MSXCPU::execute(bool fastForward)
110{
111 if (z80Active != newZ80Active) {
112 EmuTime time = getCurrentTime();
113 z80Active = newZ80Active;
114 z80Active ? z80 ->warp(time)
115 : r800->warp(time);
116 // copy Z80/R800 cache lines
117 auto zCache = z80 ->getCacheLines();
118 auto rCache = r800->getCacheLines();
119 auto from = z80Active ? rCache : zCache;
120 auto to = z80Active ? zCache : rCache;
121 std::copy_n(from.read, CacheLine::NUM, to.read );
122 std::copy_n(from.write, CacheLine::NUM, to.write);
123 }
124 z80Active ? z80 ->execute(fastForward)
125 : r800->execute(fastForward);
126}
127
129{
130 z80Active ? z80 ->exitCPULoopSync()
131 : r800->exitCPULoopSync();
132}
134{
135 z80Active ? z80 ->exitCPULoopAsync()
136 : r800->exitCPULoopAsync();
137}
138
139EmuTime::param MSXCPU::getCurrentTime() const
140{
141 return z80Active ? z80 ->getCurrentTime()
142 : r800->getCurrentTime();
143}
144
145void MSXCPU::setNextSyncPoint(EmuTime::param time)
146{
147 z80Active ? z80 ->setNextSyncPoint(time)
148 : r800->setNextSyncPoint(time);
149}
150
151void MSXCPU::invalidateMemCacheSlot()
152{
153 ranges::fill(slots, 0);
154
155 // nullptr: means not a valid entry and not yet attempted to fill this entry
156 for (auto i : xrange(16)) {
157 ranges::fill(slotReadLines[i], nullptr);
158 ranges::fill(slotWriteLines[i], nullptr);
159 }
160}
161
162void MSXCPU::updateVisiblePage(byte page, byte primarySlot, byte secondarySlot)
163{
164 byte from = slots[page];
165 byte to = 4 * primarySlot + secondarySlot;
166 slots[page] = to;
167
168 auto [cpuReadLines, cpuWriteLines] = z80Active ? z80->getCacheLines() : r800->getCacheLines();
169
170 unsigned first = page * (0x4000 / CacheLine::SIZE);
171 unsigned num = 0x4000 / CacheLine::SIZE;
172 std::copy_n(&cpuReadLines [first], num, &slotReadLines [from][first]);
173 std::copy_n(&slotReadLines [to][first], num, &cpuReadLines [first]);
174 std::copy_n(&cpuWriteLines [first], num, &slotWriteLines[from][first]);
175 std::copy_n(&slotWriteLines[to][first], num, &cpuWriteLines [first]);
176
177 if (r800) r800->updateVisiblePage(page, primarySlot, secondarySlot);
178}
179
181{
182 if (interface) interface->tick(CacheLineCounters::InvalidateAllSlots);
183 auto [cpuReadLines, cpuWriteLines] = z80Active ? z80->getCacheLines() : r800->getCacheLines();
184
185 unsigned first = start / CacheLine::SIZE;
186 unsigned num = (size + CacheLine::SIZE - 1) / CacheLine::SIZE;
187 std::fill_n(cpuReadLines + first, num, nullptr); // nullptr: means not a valid entry and not
188 std::fill_n(cpuWriteLines + first, num, nullptr); // yet attempted to fill this entry
189
190 for (auto i : xrange(16)) {
191 std::fill_n(slotReadLines [i] + first, num, nullptr);
192 std::fill_n(slotWriteLines[i] + first, num, nullptr);
193 }
194}
195
196template<bool READ, bool WRITE, bool SUB_START>
197void MSXCPU::setRWCache(unsigned start, unsigned size, const byte* rData, byte* wData, int ps, int ss,
198 const byte* disallowRead, const byte* disallowWrite)
199{
200 if constexpr (!SUB_START) {
201 assert(rData == nullptr);
202 assert(wData == nullptr);
203 }
204
205 // aligned on cache lines
206 assert((start & CacheLine::LOW) == 0);
207 assert((size & CacheLine::LOW) == 0);
208
209 int slot = 4 * ps + ss;
210 unsigned page = start >> 14;
211 assert(((start + size - 1) >> 14) == page); // all in same page
212 if constexpr (SUB_START && READ) rData -= start;
213 if constexpr (SUB_START && WRITE) wData -= start;
214
215 // select between 'active' or 'shadow' cache lines
216 auto [readLines, writeLines] = [&] {
217 if (slot == slots[page]) {
218 return z80Active ? z80->getCacheLines() : r800->getCacheLines();
219 } else {
220 return CacheLines{slotReadLines [slot],
221 slotWriteLines[slot]};
222 }
223 }();
224
225 unsigned first = start / CacheLine::SIZE;
226 readLines += first;
227 writeLines += first;
228 disallowRead += first;
229 disallowWrite += first;
230 unsigned num = size / CacheLine::SIZE;
231
232 static auto* const NON_CACHEABLE = reinterpret_cast<byte*>(1);
233 for (auto i : xrange(num)) {
234 if constexpr (READ) readLines [i] = disallowRead [i] ? NON_CACHEABLE : rData;
235 if constexpr (WRITE) writeLines[i] = disallowWrite[i] ? NON_CACHEABLE : wData;
236 }
237}
238
239static constexpr void extendForAlignment(unsigned& start, unsigned& size)
240{
241 constexpr unsigned MASK = ~(CacheLine::LOW); // not CacheLine::HIGH because 0x0000ff00 != 0xffffff00
242
243 auto end = start + size;
244 start &= MASK; // round down to cacheline
245 end = (end + CacheLine::SIZE - 1) & MASK; // round up to cacheline
246 size = end - start;
247}
248
249void MSXCPU::invalidateRWCache(unsigned start, unsigned size, int ps, int ss,
250 const byte* disallowRead, const byte* disallowWrite)
251{
252 // unaligned [start, start+size) is OK for invalidate, then simply invalidate a little more.
253 extendForAlignment(start, size);
254 setRWCache<true, true, false>(start, size, nullptr, nullptr, ps, ss, disallowRead, disallowWrite);
255}
256void MSXCPU::invalidateRCache(unsigned start, unsigned size, int ps, int ss,
257 const byte* disallowRead)
258{
259 extendForAlignment(start, size);
260 setRWCache<true, false, false>(start, size, nullptr, nullptr, ps, ss, disallowRead, nullptr);
261}
262void MSXCPU::invalidateWCache(unsigned start, unsigned size, int ps, int ss,
263 const byte* disallowWrite)
264{
265 extendForAlignment(start, size);
266 setRWCache<false, true, false>(start, size, nullptr, nullptr, ps, ss, nullptr, disallowWrite);
267}
268
269void MSXCPU::fillRWCache(unsigned start, unsigned size, const byte* rData, byte* wData, int ps, int ss,
270 const byte* disallowRead, const byte* disallowWrite)
271{
272 setRWCache<true, true, true>(start, size, rData, wData, ps, ss, disallowRead, disallowWrite);
273}
274void MSXCPU::fillRCache(unsigned start, unsigned size, const byte* rData, int ps, int ss,
275 const byte* disallowRead)
276{
277 setRWCache<true, false, true>(start, size, rData, nullptr, ps, ss, disallowRead, nullptr);
278}
279void MSXCPU::fillWCache(unsigned start, unsigned size, byte* wData, int ps, int ss,
280 const byte* disallowWrite)
281{
282 setRWCache<false, true, true>(start, size, nullptr, wData, ps, ss, nullptr, disallowWrite);
283}
284
286{
287 z80 ->raiseIRQ();
288 if (r800) r800->raiseIRQ();
289}
291{
292 z80 ->lowerIRQ();
293 if (r800) r800->lowerIRQ();
294}
296{
297 z80 ->raiseNMI();
298 if (r800) r800->raiseNMI();
299}
301{
302 z80 ->lowerNMI();
303 if (r800) r800->lowerNMI();
304}
305
306bool MSXCPU::isM1Cycle(unsigned address) const
307{
308 return z80Active ? z80 ->isM1Cycle(address)
309 : r800->isM1Cycle(address);
310}
311
312void MSXCPU::setZ80Freq(unsigned freq)
313{
314 z80->setFreq(freq);
315}
316
317void MSXCPU::wait(EmuTime::param time)
318{
319 z80Active ? z80 ->wait(time)
320 : r800->wait(time);
321}
322
323EmuTime MSXCPU::waitCyclesZ80(EmuTime::param time, unsigned cycles)
324{
325 return z80Active ? z80 ->waitCycles(time, cycles)
326 : time;
327}
328
329EmuTime MSXCPU::waitCyclesR800(EmuTime::param time, unsigned cycles)
330{
331 return z80Active ? time
332 : r800->waitCycles(time, cycles);
333}
334
336{
337 if (z80Active) {
338 return *z80;
339 } else {
340 return *r800;
341 }
342}
343
344void MSXCPU::update(const Setting& setting) noexcept
345{
346 z80 ->update(setting);
347 if (r800) r800->update(setting);
348 exitCPULoopSync();
349}
350
351// Command
352
354 Interpreter& interp, std::span<const TclObject> tokens,
355 TclObject& result) const
356{
357 z80Active ? z80 ->disasmCommand(interp, tokens, result)
358 : r800->disasmCommand(interp, tokens, result);
359}
360
361void MSXCPU::setPaused(bool paused)
362{
363 if (z80Active) {
364 z80 ->setExtHALT(paused);
365 z80 ->exitCPULoopSync();
366 } else {
367 r800->setExtHALT(paused);
368 r800->exitCPULoopSync();
369 }
370}
371
372
373// class TimeInfoTopic
374
375MSXCPU::TimeInfoTopic::TimeInfoTopic(InfoCommand& machineInfoCommand)
376 : InfoTopic(machineInfoCommand, "time")
377{
378}
379
380void MSXCPU::TimeInfoTopic::execute(
381 std::span<const TclObject> /*tokens*/, TclObject& result) const
382{
383 auto& cpu = OUTER(MSXCPU, timeInfo);
384 EmuDuration dur = cpu.getCurrentTime() - cpu.reference;
385 result = dur.toDouble();
386}
387
388std::string MSXCPU::TimeInfoTopic::help(std::span<const TclObject> /*tokens*/) const
389{
390 return "Prints the time in seconds that the MSX is powered on\n";
391}
392
393
394// class CPUFreqInfoTopic
395
396MSXCPU::CPUFreqInfoTopic::CPUFreqInfoTopic(
397 InfoCommand& machineInfoCommand,
398 const std::string& name_, CPUClock& clock_)
399 : InfoTopic(machineInfoCommand, name_)
400 , clock(clock_)
401{
402}
403
404void MSXCPU::CPUFreqInfoTopic::execute(
405 std::span<const TclObject> /*tokens*/, TclObject& result) const
406{
407 result = clock.getFreq();
408}
409
410std::string MSXCPU::CPUFreqInfoTopic::help(std::span<const TclObject> /*tokens*/) const
411{
412 return "Returns the actual frequency of this CPU.\n"
413 "This frequency can vary because:\n"
414 " - the user has overridden the freq via the '{z80,r800}_freq' setting\n"
415 " - (only on some MSX machines) the MSX software can switch the Z80 between 2 frequencies\n"
416 "See also the '{z80,r800}_freq_locked' setting.\n";
417}
418
419
420// class Debuggable
421
423 "Registers of the active CPU (Z80 or R800).\n"
424 "Each byte in this debuggable represents one 8 bit register:\n"
425 " 0 -> A 1 -> F 2 -> B 3 -> C\n"
426 " 4 -> D 5 -> E 6 -> H 7 -> L\n"
427 " 8 -> A' 9 -> F' 10 -> B' 11 -> C'\n"
428 " 12 -> D' 13 -> E' 14 -> H' 15 -> L'\n"
429 " 16 -> IXH 17 -> IXL 18 -> IYH 19 -> IYL\n"
430 " 20 -> PCH 21 -> PCL 22 -> SPH 23 -> SPL\n"
431 " 24 -> I 25 -> R 26 -> IM 27 -> IFF1/2\n"
432 "The last position (27) contains the IFF1 and IFF2 flags in respectively\n"
433 "bit 0 and 1. Bit 2 contains 'IFF1 AND last-instruction-was-not-EI', so\n"
434 "this effectively indicates that the CPU could accept an interrupt at\n"
435 "the start of the current instruction.\n";
436
437MSXCPU::Debuggable::Debuggable(MSXMotherBoard& motherboard_)
438 : SimpleDebuggable(motherboard_, "CPU regs", CPU_REGS_DESC, 28)
439{
440}
441
442byte MSXCPU::Debuggable::read(unsigned address)
443{
444 auto& cpu = OUTER(MSXCPU, debuggable);
445 const CPURegs& regs = cpu.getRegisters();
446 switch (address) {
447 case 0: return regs.getA();
448 case 1: return regs.getF();
449 case 2: return regs.getB();
450 case 3: return regs.getC();
451 case 4: return regs.getD();
452 case 5: return regs.getE();
453 case 6: return regs.getH();
454 case 7: return regs.getL();
455 case 8: return regs.getA2();
456 case 9: return regs.getF2();
457 case 10: return regs.getB2();
458 case 11: return regs.getC2();
459 case 12: return regs.getD2();
460 case 13: return regs.getE2();
461 case 14: return regs.getH2();
462 case 15: return regs.getL2();
463 case 16: return regs.getIXh();
464 case 17: return regs.getIXl();
465 case 18: return regs.getIYh();
466 case 19: return regs.getIYl();
467 case 20: return regs.getPCh();
468 case 21: return regs.getPCl();
469 case 22: return regs.getSPh();
470 case 23: return regs.getSPl();
471 case 24: return regs.getI();
472 case 25: return regs.getR();
473 case 26: return regs.getIM();
474 case 27: return 1 * regs.getIFF1() +
475 2 * regs.getIFF2() +
476 4 * (regs.getIFF1() && !regs.prevWasEI());
477 default: UNREACHABLE; return 0;
478 }
479}
480
481void MSXCPU::Debuggable::write(unsigned address, byte value)
482{
483 auto& cpu = OUTER(MSXCPU, debuggable);
484 CPURegs& regs = cpu.getRegisters();
485 switch (address) {
486 case 0: regs.setA(value); break;
487 case 1: regs.setF(value); break;
488 case 2: regs.setB(value); break;
489 case 3: regs.setC(value); break;
490 case 4: regs.setD(value); break;
491 case 5: regs.setE(value); break;
492 case 6: regs.setH(value); break;
493 case 7: regs.setL(value); break;
494 case 8: regs.setA2(value); break;
495 case 9: regs.setF2(value); break;
496 case 10: regs.setB2(value); break;
497 case 11: regs.setC2(value); break;
498 case 12: regs.setD2(value); break;
499 case 13: regs.setE2(value); break;
500 case 14: regs.setH2(value); break;
501 case 15: regs.setL2(value); break;
502 case 16: regs.setIXh(value); break;
503 case 17: regs.setIXl(value); break;
504 case 18: regs.setIYh(value); break;
505 case 19: regs.setIYl(value); break;
506 case 20: regs.setPCh(value); break;
507 case 21: regs.setPCl(value); break;
508 case 22: regs.setSPh(value); break;
509 case 23: regs.setSPl(value); break;
510 case 24: regs.setI(value); break;
511 case 25: regs.setR(value); break;
512 case 26:
513 if (value < 3) regs.setIM(value);
514 break;
515 case 27:
516 regs.setIFF1((value & 0x01) != 0);
517 regs.setIFF2((value & 0x02) != 0);
518 // can't change afterEI
519 break;
520 default:
522 }
523}
524
525// version 1: initial version
526// version 2: activeCPU,newCPU -> z80Active,newZ80Active
527template<typename Archive>
528void MSXCPU::serialize(Archive& ar, unsigned version)
529{
530 if (ar.versionAtLeast(version, 2)) {
531 ar.serialize("z80", *z80);
532 if (r800) ar.serialize("r800", *r800);
533 ar.serialize("z80Active", z80Active,
534 "newZ80Active", newZ80Active);
535 } else {
536 // backwards-compatibility
537 assert(Archive::IS_LOADER);
538
539 ar.serializeWithID("z80", *z80);
540 if (r800) ar.serializeWithID("r800", *r800);
541 CPUBase* activeCPU = nullptr;
542 CPUBase* newCPU = nullptr;
543 ar.serializePointerID("activeCPU", activeCPU);
544 ar.serializePointerID("newCPU", newCPU);
545 z80Active = activeCPU == z80.get();
546 if (newCPU) {
547 newZ80Active = newCPU == z80.get();
548 } else {
549 newZ80Active = z80Active;
550 }
551 }
552 ar.serialize("resetTime", reference);
553
554 if constexpr (Archive::IS_LOADER) {
555 invalidateMemCacheSlot();
556 invalidateAllSlotsRWCache(0x0000, 0x10000);
557 }
558}
560
561} // namespace openmsx
BaseSetting * setting
Definition: Interpreter.cc:27
void tick(ENUM e) const
void setCPU(MSXCPU *cpu_)
Definition: Debugger.hh:40
void setZ80Freq(unsigned freq)
Switch the Z80 clock freq.
Definition: MSXCPU.cc:312
void fillWCache(unsigned start, unsigned size, byte *wData, int ps, int ss, const byte *disallowWrite)
Definition: MSXCPU.cc:279
bool isM1Cycle(unsigned address) const
Should only be used from within a MSXDevice::readMem() method.
Definition: MSXCPU.cc:306
void lowerNMI()
This methods lowers the non-maskable interrupt again.
Definition: MSXCPU.cc:300
void fillRWCache(unsigned start, unsigned size, const byte *rData, byte *wData, int ps, int ss, const byte *disallowRead, const byte *disallowWrite)
Fill the read and write cache lines for a specific slot with the specified value.
Definition: MSXCPU.cc:269
void setActiveCPU(CPUType cpu)
Switch between Z80/R800.
Definition: MSXCPU.cc:92
void invalidateAllSlotsRWCache(word start, unsigned size)
Invalidate the CPU its cache for the interval [start, start + size) For example MSXMemoryMapper and M...
Definition: MSXCPU.cc:180
void fillRCache(unsigned start, unsigned size, const byte *rData, int ps, int ss, const byte *disallowRead)
Definition: MSXCPU.cc:274
void updateVisiblePage(byte page, byte primarySlot, byte secondarySlot)
Inform CPU of bank switch.
Definition: MSXCPU.cc:162
void invalidateRWCache(unsigned start, unsigned size, int ps, int ss, const byte *disallowRead, const byte *disallowWrite)
Similar to the method above, but only invalidates one specific slot.
Definition: MSXCPU.cc:249
CPURegs & getRegisters()
Definition: MSXCPU.cc:335
EmuTime waitCyclesR800(EmuTime::param time, unsigned cycles)
Definition: MSXCPU.cc:329
void exitCPULoopSync()
See CPUCore::exitCPULoopsync()
Definition: MSXCPU.cc:128
void disasmCommand(Interpreter &interp, std::span< const TclObject > tokens, TclObject &result) const
Definition: MSXCPU.cc:353
void setNextSyncPoint(EmuTime::param time)
Definition: MSXCPU.cc:145
EmuTime waitCyclesZ80(EmuTime::param time, unsigned cycles)
Definition: MSXCPU.cc:323
void raiseIRQ()
This method raises a maskable interrupt.
Definition: MSXCPU.cc:285
void raiseNMI()
This method raises a non-maskable interrupt.
Definition: MSXCPU.cc:295
void wait(EmuTime::param time)
Definition: MSXCPU.cc:317
void invalidateRCache(unsigned start, unsigned size, int ps, int ss, const byte *disallowRead)
Definition: MSXCPU.cc:256
void doReset(EmuTime::param time)
Reset CPU.
Definition: MSXCPU.cc:82
void setInterface(MSXCPUInterface *interf)
Definition: MSXCPU.cc:75
void setPaused(bool paused)
(un)pause CPU.
Definition: MSXCPU.cc:361
void exitCPULoopAsync()
See CPUCore::exitCPULoopAsync()
Definition: MSXCPU.cc:133
void setDRAMmode(bool dram)
Sets DRAM or ROM mode (influences memory access speed for R800).
Definition: MSXCPU.cc:103
void lowerIRQ()
This methods lowers the maskable interrupt again.
Definition: MSXCPU.cc:290
MSXCPU(MSXMotherBoard &motherboard)
Definition: MSXCPU.cc:21
void invalidateWCache(unsigned start, unsigned size, int ps, int ss, const byte *disallowWrite)
Definition: MSXCPU.cc:262
void setCPU(MSXCPU *cpu_)
Definition: Scheduler.hh:40
void detach(Observer< T > &observer)
Definition: Subject.hh:56
void attach(Observer< T > &observer)
Definition: Subject.hh:50
static_string_view
constexpr unsigned NUM
Definition: CacheLine.hh:8
constexpr unsigned LOW
Definition: CacheLine.hh:9
constexpr unsigned SIZE
Definition: CacheLine.hh:7
This file implemented 3 utility functions:
Definition: Autofire.cc:9
@ InvalidateAllSlots
constexpr static_string_view CPU_REGS_DESC
Definition: MSXCPU.cc:422
uint16_t word
16 bit unsigned integer
Definition: openmsx.hh:29
void serialize(Archive &ar, T &t, unsigned version)
constexpr void fill(ForwardRange &&range, const T &value)
Definition: ranges.hh:256
STL namespace.
size_t size(std::string_view utf8)
#define OUTER(type, member)
Definition: outer.hh:41
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:1009
#define UNREACHABLE
Definition: unreachable.hh:38
constexpr auto xrange(T e)
Definition: xrange.hh:133
constexpr auto end(const zstring_view &x)