openMSX
MSXDevice.cc
Go to the documentation of this file.
1#include "MSXDevice.hh"
2#include "XMLElement.hh"
3#include "MSXMotherBoard.hh"
4#include "HardwareConfig.hh"
6#include "MSXCPUInterface.hh"
7#include "CacheLine.hh"
8#include "TclObject.hh"
9#include "MSXException.hh"
10#include "one_of.hh"
11#include "ranges.hh"
12#include "serialize.hh"
13#include "stl.hh"
14#include "unreachable.hh"
15#include "xrange.hh"
16#include <bit>
17#include <cassert>
18
19namespace openmsx {
20
21MSXDevice::MSXDevice(const DeviceConfig& config, std::string_view name)
22 : deviceConfig(config)
23{
24 initName(name);
25}
26
28 : deviceConfig(config)
29{
30 initName(getDeviceConfig().getAttributeValue("id"));
31}
32
33void MSXDevice::initName(std::string_view name)
34{
35 deviceName = name;
36 if (getMotherBoard().findDevice(deviceName)) {
37 unsigned n = 0;
38 do {
39 deviceName = strCat(name, " (", ++n, ')');
40 } while (getMotherBoard().findDevice(deviceName));
41 }
42}
43
45{
46 staticInit();
47
48 lockDevices();
49 registerSlots();
50 registerPorts();
51}
52
54{
55 unregisterPorts();
56 unregisterSlots();
57 unlockDevices();
58 assert(referencedBy.empty());
59}
60
61void MSXDevice::staticInit()
62{
63 static bool alreadyInit = false;
64 if (alreadyInit) return;
65 alreadyInit = true;
66
68}
69
74
75void MSXDevice::testRemove(std::span<const std::unique_ptr<MSXDevice>> removed) const
76{
77 // Typically 'referencedBy' contains very few elements, so a simple
78 // O(n*m) algorithm is fine.
79 std::string err;
80 for (const auto* dev : referencedBy) {
81 if (!contains(removed, dev, [](const auto& d) { return d.get(); })) {
82 strAppend(err, ' ', dev->getName());
83 }
84 }
85 if (!err.empty()) {
86 throw MSXException("Still in use by:", err);
87 }
88}
89
90void MSXDevice::lockDevices()
91{
92 // This code can only handle backward references: the thing that is
93 // referenced must already be instantiated, we don't try to change the
94 // instantiation order. For the moment this is good enough (only ADVRAM
95 // (an extension) uses it to refer to the VDP (inside a machine)). If
96 // needed we can implement something more sophisticated later without
97 // changing the format of the config files.
98 for (const auto* c : getDeviceConfig().getChildren("device")) {
99 auto name = c->getAttributeValue("idref");
100 auto* dev = getMotherBoard().findDevice(name);
101 if (!dev) {
102 throw MSXException(
103 "Unsatisfied dependency: '", getName(),
104 "' depends on unavailable device '",
105 name, "'.");
106 }
107 references.push_back(dev);
108 dev->referencedBy.push_back(this);
109 }
110}
111
112void MSXDevice::unlockDevices()
113{
114 for (auto& r : references) {
115 move_pop_back(r->referencedBy, rfind_unguarded(r->referencedBy, this));
116 }
117}
118
120{
121 // init() must already be called
122 return references;
123}
124
125EmuTime::param MSXDevice::getCurrentTime() const
126{
128}
130{
131 return getMotherBoard().getCPU();
132}
146{
147 return getMotherBoard().getReactor();
148}
161
162void MSXDevice::registerSlots()
163{
164 MemRegions tmpMemRegions;
165 unsigned align = getBaseSizeAlignment();
166 assert(std::has_single_bit(align));
167 for (const auto* m : getDeviceConfig().getChildren("mem")) {
168 unsigned base = m->getAttributeValueAsInt("base", 0);
169 unsigned size = m->getAttributeValueAsInt("size", 0);
170 if ((base >= 0x10000) || (size > 0x10000) || ((base + size) > 0x10000)) {
171 throw MSXException(
172 "Invalid memory specification for device ",
173 getName(), " should be in range "
174 "[0x0000,0x10000).");
175 }
176 if (((base & (align - 1)) || (size & (align - 1))) &&
177 !allowUnaligned()) {
178 throw MSXException(
179 "invalid memory specification for device ",
180 getName(), " should be aligned on at least 0x",
181 hex_string<4>(align), '.');
182 }
183 tmpMemRegions.emplace_back(BaseSize{base, size});
184 }
185 if (tmpMemRegions.empty()) {
186 return;
187 }
188
189 // find primary and secondary slot specification
190 auto& slotManager = getMotherBoard().getSlotManager();
191 auto* primaryConfig = getDeviceConfig2().getPrimary();
192 auto* secondaryConfig = getDeviceConfig2().getSecondary();
193 if (primaryConfig) {
194 ps = slotManager.getSlotNum(primaryConfig->getAttributeValue("slot"));
195 } else {
196 throw MSXException("Invalid memory specification");
197 }
198 if (secondaryConfig) {
199 auto ss_str = secondaryConfig->getAttributeValue("slot");
200 ss = slotManager.getSlotNum(ss_str);
201 if ((-16 <= ss) && (ss <= -1) && (ss != ps)) {
202 throw MSXException(
203 "Invalid secondary slot specification: \"",
204 ss_str, "\".");
205 }
206 } else {
207 ss = 0;
208 }
209
210 // This is only for backwards compatibility: in the past we added extra
211 // attributes "primary_slot" and "secondary_slot" (in each MSXDevice
212 // config) instead of changing the 'any' value of the slot attribute of
213 // the (possibly shared) <primary> and <secondary> tags. When loading
214 // an old savestate these tags can still occur, so keep this code. Also
215 // remove these attributes to convert to the new format.
216 auto& mutableConfig = const_cast<XMLElement&>(getDeviceConfig());
217 if (auto** primSlotPtr = mutableConfig.findAttributePointer("primary_slot")) {
218 ps = slotManager.getSlotNum((*primSlotPtr)->getValue());
219 mutableConfig.removeAttribute(primSlotPtr);
220 if (auto** secSlotPtr = mutableConfig.findAttributePointer("secondary_slot")) {
221 ss = slotManager.getSlotNum((*secSlotPtr)->getValue());
222 mutableConfig.removeAttribute(secSlotPtr);
223 }
224 }
225
226 // decode special values for 'ss'
227 if ((-128 <= ss) && (ss < 0)) {
228 if ((0 <= ps) && (ps < 4) &&
230 ss += 128;
231 } else {
232 ss = 0;
233 }
234 }
235
236 // decode special values for 'ps'
237 if (ps == -256) {
238 slotManager.getAnyFreeSlot(ps, ss);
239 } else if (ps < 0) {
240 // specified slot by name (carta, cartb, ...)
241 slotManager.getSpecificSlot(-ps - 1, ps, ss);
242 } else {
243 // numerical specified slot (0, 1, 2, 3)
244 }
245 assert((0 <= ps) && (ps <= 3));
246
247 if (!getCPUInterface().isExpanded(ps)) {
248 ss = -1;
249 }
250
251 // Store actual slot in config. This has two purposes:
252 // - Make sure that devices that are grouped under the same
253 // <primary>/<secondary> tags actually use the same slot. (This
254 // matters when the value of some of the slot attributes is "any").
255 // - Fix the slot number so that it remains the same after a
256 // savestate/loadstate.
257 assert(primaryConfig);
258 XMLDocument& doc = deviceConfig.getXMLDocument();
259 doc.setAttribute(*primaryConfig, "slot", doc.allocateString(tmpStrCat(ps)));
260 if (secondaryConfig) {
261 doc.setAttribute(*secondaryConfig, "slot", doc.allocateString(
262 (ss == -1) ? std::string_view("X") : tmpStrCat(ss)));
263 } else {
264 if (ss != -1) {
265 throw MSXException(
266 "Missing <secondary> tag for device", getName());
267 }
268 }
269
270 int logicalSS = (ss == -1) ? 0 : ss;
271 for (const auto& r : tmpMemRegions) {
273 *this, ps, logicalSS, r.base, r.size);
274 memRegions.push_back(r);
275 }
276
277 // Mark the slot as 'in-use' so that future searches for free external
278 // slots don't return this slot anymore. If the slot was not an
279 // external slot, this call has no effect. Multiple MSXDevices from the
280 // same extension (the same HardwareConfig) can all allocate the same
281 // slot (later they should also all free this slot).
282 slotManager.allocateSlot(ps, ss, getHardwareConfig());
283}
284
285void MSXDevice::unregisterSlots()
286{
287 if (memRegions.empty()) return;
288
289 int logicalSS = (ss == -1) ? 0 : ss;
290 for (const auto& [base, size] : memRegions) {
292 *this, ps, logicalSS, base, size);
293 }
294
295 // See comments above about allocateSlot() for more details:
296 // - has no effect for non-external slots
297 // - can be called multiple times for the same slot
299}
300
301void MSXDevice::getVisibleMemRegion(unsigned& base, unsigned& size) const
302{
303 // init() must already be called
304 if (memRegions.empty()) {
305 base = 0;
306 size = 0;
307 return;
308 }
309
310 auto lowest = min_value(memRegions, &BaseSize::base);
311 auto highest = max_value(memRegions, &BaseSize::end);
312 assert(lowest <= highest);
313 base = lowest;
314 size = highest - lowest;
315}
316
317void MSXDevice::registerPorts()
318{
319 // First calculate 'inPort' and 'outPorts' ..
320 for (const auto* i : getDeviceConfig().getChildren("io")) {
321 unsigned base = i->getAttributeValueAsInt("base", 0);
322 unsigned num = i->getAttributeValueAsInt("num", 0);
323 auto type = i->getAttributeValue("type", "IO");
324 if (((base + num) > 256) || (num == 0) ||
325 (type != one_of("I", "O", "IO"))) {
326 throw MSXException("Invalid IO port specification");
327 }
328 if (type != "O") { // "I" or "IO"
329 inPorts.setPosN(base, num);
330 }
331 if (type != "I") { // "O" or "IO"
332 outPorts.setPosN(base, num);
333 }
334 }
335 // .. and only then register the ports. This filters possible overlaps.
336 inPorts.foreachSetBit([&](auto port) {
337 getCPUInterface().register_IO_In(narrow_cast<byte>(port), this);
338 });
339 outPorts.foreachSetBit([&](auto port) {
340 getCPUInterface().register_IO_Out(narrow_cast<byte>(port), this);
341 });
342}
343
344void MSXDevice::unregisterPorts()
345{
346 inPorts.foreachSetBit([&](auto port) {
347 getCPUInterface().unregister_IO_In(narrow_cast<byte>(port), this);
348 });
349 outPorts.foreachSetBit([&](auto port) {
350 getCPUInterface().unregister_IO_Out(narrow_cast<byte>(port), this);
351 });
352}
353
354
355void MSXDevice::reset(EmuTime::param /*time*/)
356{
357 // nothing
358}
359
361{
362 return 0xFF;
363}
364
365void MSXDevice::powerDown(EmuTime::param /*time*/)
366{
367 // nothing
368}
369
370void MSXDevice::powerUp(EmuTime::param time)
371{
372 reset(time);
373}
374
375const std::string& MSXDevice::getName() const
376{
377 return deviceName;
378}
379
381{
382 result.addListElement(getName());
383}
384
386{
387 result.addDictKeyValue("type", getDeviceConfig().getName());
388 getExtraDeviceInfo(result);
389}
390
392{
393 // nothing
394}
395
397{
398 return CacheLine::SIZE;
399}
400
401
402byte MSXDevice::readIO(word /*port*/, EmuTime::param /*time*/)
403{
404 // read from unmapped IO
405 return 0xFF;
406}
407
408void MSXDevice::writeIO(word /*port*/, byte /*value*/, EmuTime::param /*time*/)
409{
410 // write to unmapped IO, do nothing
411}
412
413byte MSXDevice::peekIO(word /*port*/, EmuTime::param /*time*/) const
414{
415 return 0xFF;
416}
417
418
419byte MSXDevice::readMem(word /*address*/, EmuTime::param /*time*/)
420{
421 // read from unmapped memory
422 return 0xFF;
423}
424
425const byte* MSXDevice::getReadCacheLine(word /*start*/) const
426{
427 return nullptr; // uncacheable
428}
429
430void MSXDevice::writeMem(word /*address*/, byte /*value*/,
431 EmuTime::param /*time*/)
432{
433 // write to unmapped memory, do nothing
434}
435
436byte MSXDevice::peekMem(word address, EmuTime::param /*time*/) const
437{
438 word base = address & CacheLine::HIGH;
439 if (const byte* cache = getReadCacheLine(base)) {
440 word offset = address & CacheLine::LOW;
441 return cache[offset];
442 } else {
443 // peek not supported for this device
444 return 0xFF;
445 }
446}
447
448void MSXDevice::globalWrite(word /*address*/, byte /*value*/,
449 EmuTime::param /*time*/)
450{
452}
453
454void MSXDevice::globalRead(word /*address*/, EmuTime::param /*time*/)
455{
457}
458
460{
461 return nullptr; // uncacheable
462}
463
464
465// calls 'action(<start2>, <size2>, args..., ps, ss)'
466// with 'start', 'size' clipped to each of the ranges in 'memRegions'
467template<typename Action, typename... Args>
468void MSXDevice::clip(unsigned start, unsigned size, Action action, Args... args)
469{
470 assert(start < 0x10000);
471 int ss2 = (ss != -1) ? ss : 0;
472 unsigned end = start + size;
473 for (auto [base, fullBsize] : memRegions) {
474 // split on 16kB boundaries
475 while (fullBsize > 0) {
476 unsigned bsize = std::min(fullBsize, ((base + 0x4000) & ~0x3fff) - base);
477
478 unsigned baseEnd = base + bsize;
479 // intersect [start, end) with [base, baseEnd) -> [clipStart, clipEnd)
480 unsigned clipStart = std::max(start, base);
481 unsigned clipEnd = std::min(end, baseEnd);
482 if (clipStart < clipEnd) { // non-empty
483 unsigned clipSize = clipEnd - clipStart;
484 action(narrow<word>(clipStart), clipSize, args..., ps, ss2);
485 }
486
487 base += bsize;
488 fullBsize -= bsize;
489 }
490 }
491}
492
493void MSXDevice::invalidateDeviceRWCache(unsigned start, unsigned size)
494{
495 clip(start, size, [&](auto... args) { getCPUInterface().invalidateRWCache(args...); });
496}
497void MSXDevice::invalidateDeviceRCache(unsigned start, unsigned size)
498{
499 clip(start, size, [&](auto... args) { getCPUInterface().invalidateRCache(args...); });
500}
501void MSXDevice::invalidateDeviceWCache(unsigned start, unsigned size)
502{
503 clip(start, size, [&](auto... args) { getCPUInterface().invalidateWCache(args...); });
504}
505
506void MSXDevice::fillDeviceRWCache(unsigned start, unsigned size, byte* rwData)
507{
508 fillDeviceRWCache(start, size, rwData, rwData);
509}
510void MSXDevice::fillDeviceRWCache(unsigned start, unsigned size, const byte* rData, byte* wData)
511{
512 assert(!allowUnaligned());
513 clip(start, size, [&](auto... args) { getCPUInterface().fillRWCache(args...); }, rData, wData);
514}
515void MSXDevice::fillDeviceRCache(unsigned start, unsigned size, const byte* rData)
516{
517 assert(!allowUnaligned());
518 clip(start, size, [&](auto... args) { getCPUInterface().fillRCache(args...); }, rData);
519}
520void MSXDevice::fillDeviceWCache(unsigned start, unsigned size, byte* wData)
521{
522 assert(!allowUnaligned());
523 clip(start, size, [&](auto... args) { getCPUInterface().fillWCache(args...); }, wData);
524}
525
526template<typename Archive>
527void MSXDevice::serialize(Archive& ar, unsigned /*version*/)
528{
529 // When this method is called, the method init() has already been
530 // called (thus also registerSlots() and registerPorts()).
531 ar.serialize("name", deviceName);
532}
534
535} // namespace openmsx
void setPosN(size_t pos, size_t n)
Starting from position 'pos', set the 'n' following bits to '1'.
void foreachSetBit(std::invocable< size_t > auto op) const
Execute the given operation 'op' for all '1' bits.
void freeSlot(int ps, int ss, const HardwareConfig &hwConfig)
XMLElement * getPrimary() const
XMLDocument & getXMLDocument()
XMLElement * getSecondary() const
MSXMotherBoard & getMotherBoard() const
void register_IO_Out(byte port, MSXDevice *device)
Devices can register their Out ports.
void invalidateRWCache(word start, unsigned size, int ps, int ss)
void register_IO_In(byte port, MSXDevice *device)
Devices can register their In ports.
void fillWCache(unsigned start, unsigned size, byte *wData, int ps, int ss)
void unregister_IO_In(byte port, MSXDevice *device)
void unregisterMemDevice(MSXDevice &device, int ps, int ss, unsigned base, unsigned size)
void invalidateRCache(word start, unsigned size, int ps, int ss)
void fillRWCache(unsigned start, unsigned size, const byte *rData, byte *wData, int ps, int ss)
void invalidateWCache(word start, unsigned size, int ps, int ss)
void registerMemDevice(MSXDevice &device, int ps, int ss, unsigned base, unsigned size)
Devices can register themself in the MSX slot structure.
void unregister_IO_Out(byte port, MSXDevice *device)
void fillRCache(unsigned start, unsigned size, const byte *rData, int ps, int ss)
bool isExpanded(int ps) const
An MSXDevice is an emulated hardware component connected to the bus of the emulated MSX.
Definition MSXDevice.hh:36
MSXMotherBoard & getMotherBoard() const
Get the mother board this device belongs to.
Definition MSXDevice.cc:70
virtual void init()
Definition MSXDevice.cc:44
void invalidateDeviceRCache()
Definition MSXDevice.hh:215
void fillDeviceWCache(unsigned start, unsigned size, byte *wData)
Definition MSXDevice.cc:520
PluggingController & getPluggingController() const
Definition MSXDevice.cc:157
virtual void powerUp(EmuTime::param time)
This method is called when MSX is powered up.
Definition MSXDevice.cc:370
static std::array< byte, 0x10000 > unmappedRead
Definition MSXDevice.hh:306
const HardwareConfig & getHardwareConfig() const
Returns the hardwareconfig this device belongs to.
Definition MSXDevice.hh:49
void fillDeviceRWCache(unsigned start, unsigned size, byte *rwData)
Calls MSXCPUInterface::fillXXCache() for the specific (part of) the slot that this device is located ...
Definition MSXDevice.cc:506
virtual ~MSXDevice()=0
Definition MSXDevice.cc:53
virtual const byte * getReadCacheLine(word start) const
Test that the memory in the interval [start, start + CacheLine::SIZE) is cacheable for reading.
Definition MSXDevice.cc:425
void invalidateDeviceWCache()
Definition MSXDevice.hh:216
void getDeviceInfo(TclObject &result) const
Get device info.
Definition MSXDevice.cc:385
virtual void writeIO(word port, byte value, EmuTime::param time)
Write a byte to a given IO port at a certain time to this device.
Definition MSXDevice.cc:408
virtual byte readIRQVector()
Gets IRQ vector used in IM2.
Definition MSXDevice.cc:360
virtual void globalWrite(word address, byte value, EmuTime::param time)
Global writes.
Definition MSXDevice.cc:448
Reactor & getReactor() const
Definition MSXDevice.cc:145
virtual bool allowUnaligned() const
By default we don't allow unaligned <mem> specifications in the config file.
Definition MSXDevice.hh:296
const DeviceConfig & getDeviceConfig2() const
Definition MSXDevice.hh:239
virtual unsigned getBaseSizeAlignment() const
The 'base' and 'size' attribute values need to be at least aligned to CacheLine::SIZE.
Definition MSXDevice.cc:396
MSXDevice(const MSXDevice &)=delete
std::vector< MSXDevice * > Devices
Definition MSXDevice.hh:43
virtual byte readMem(word address, EmuTime::param time)
Read a byte from a location at a certain time from this device.
Definition MSXDevice.cc:419
virtual void getExtraDeviceInfo(TclObject &result) const
Definition MSXDevice.cc:391
std::string deviceName
Definition MSXDevice.hh:326
void getVisibleMemRegion(unsigned &base, unsigned &size) const
Returns the range where this device is visible in memory.
Definition MSXDevice.cc:301
virtual void writeMem(word address, byte value, EmuTime::param time)
Write a given byte to a given location at a certain time to this device.
Definition MSXDevice.cc:430
virtual byte peekIO(word port, EmuTime::param time) const
Read a byte from a given IO port.
Definition MSXDevice.cc:413
virtual byte peekMem(word address, EmuTime::param time) const
Read a byte from a given memory location.
Definition MSXDevice.cc:436
MSXCPU & getCPU() const
Definition MSXDevice.cc:129
virtual const std::string & getName() const
Returns a human-readable name for this device.
Definition MSXDevice.cc:375
LedStatus & getLedStatus() const
Definition MSXDevice.cc:153
void serialize(Archive &ar, unsigned version)
Definition MSXDevice.cc:527
void invalidateDeviceRWCache()
Calls MSXCPUInterface::invalidateXXCache() for the specific (part of) the slot that this device is lo...
Definition MSXDevice.hh:214
Scheduler & getScheduler() const
Definition MSXDevice.cc:137
virtual void getNameList(TclObject &result) const
Returns list of name(s) of this device.
Definition MSXDevice.cc:380
void testRemove(std::span< const std::unique_ptr< MSXDevice > > removed) const
Checks whether this device can be removed (no other device has a reference to it).
Definition MSXDevice.cc:75
virtual byte * getWriteCacheLine(word start)
Test that the memory in the interval [start, start + CacheLine::SIZE) is cacheable for writing.
Definition MSXDevice.cc:459
const XMLElement & getDeviceConfig() const
Get the configuration section for this device.
Definition MSXDevice.hh:236
void fillDeviceRCache(unsigned start, unsigned size, const byte *rData)
Definition MSXDevice.cc:515
CommandController & getCommandController() const
Definition MSXDevice.cc:149
const Devices & getReferences() const
Get the device references that are specified for this device.
Definition MSXDevice.cc:119
virtual void powerDown(EmuTime::param time)
This method is called when MSX is powered down.
Definition MSXDevice.cc:365
virtual void globalRead(word address, EmuTime::param time)
Global reads.
Definition MSXDevice.cc:454
virtual void reset(EmuTime::param time)
This method is called on reset.
Definition MSXDevice.cc:355
EmuTime::param getCurrentTime() const
Definition MSXDevice.cc:125
MSXCPUInterface & getCPUInterface() const
Definition MSXDevice.cc:133
virtual byte readIO(word port, EmuTime::param time)
Read a byte from an IO port at a certain time from this device.
Definition MSXDevice.cc:402
MSXCliComm & getCliComm() const
Definition MSXDevice.cc:141
PluggingController & getPluggingController()
EmuTime::param getCurrentTime() const
Convenience method: This is the same as getScheduler().getCurrentTime().
MSXCPUInterface & getCPUInterface()
MSXDevice * findDevice(std::string_view name)
Find a MSXDevice by name.
CommandController & getCommandController()
CartridgeSlotManager & getSlotManager()
Central administration of Connectors and Pluggables.
Contains the main loop of openMSX.
Definition Reactor.hh:75
void addListElement(const T &t)
Definition TclObject.hh:133
void addDictKeyValue(const Key &key, const Value &value)
Definition TclObject.hh:147
void setAttribute(XMLElement &elem, const char *attrName, const char *attrValue)
constexpr unsigned LOW
Definition CacheLine.hh:9
constexpr unsigned HIGH
Definition CacheLine.hh:10
constexpr unsigned SIZE
Definition CacheLine.hh:7
This file implemented 3 utility functions:
Definition Autofire.cc:11
uint16_t word
16 bit unsigned integer
Definition openmsx.hh:29
constexpr void fill(ForwardRange &&range, const T &value)
Definition ranges.hh:315
size_t size(std::string_view utf8)
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
auto min_value(InputIterator first, InputIterator last, Proj proj={})
Definition stl.hh:208
auto max_value(InputIterator first, InputIterator last, Proj proj={})
Definition stl.hh:227
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition stl.hh:137
auto rfind_unguarded(RANGE &range, const VAL &val, Proj proj={})
Similar to the find(_if)_unguarded functions above, but searches from the back to front.
Definition stl.hh:112
constexpr bool contains(ITER first, ITER last, const VAL &val)
Check if a range contains a given value, using linear search.
Definition stl.hh:35
std::string strCat()
Definition strCat.hh:703
TemporaryString tmpStrCat(Ts &&... ts)
Definition strCat.hh:742
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752
#define UNREACHABLE
constexpr auto end(const zstring_view &x)