[...] Commodore 64 did indeed provide 64K of RAM [...] only 38K was usable from BASIC; [...] bank switching was needed to get at the rest,
And that's exactly the way the TED computers did use.
but if a BASIC program switched out the [...] ROM, [...] it would of course promptly crash.
Well, BASIC 3.5 was aware of that and did provide functions for various RAM und ROM access (*1).
But the Commodore Plus/4 apparently provided 60K to BASIC programs. By the above reasoning, this should be impossible.
How did it work?
Well, what about because it did exactly do so and switched out the ROMs?
It's a feature of BASIC 3.5 and all TED computers using it (C16, 116, Plus/4). For read access (*2) to all RAM (*3) the ROMs need to be switched out, and back again afterwards. To do so an access stub was established in RAM (see below), itself unaffected by the switch. When called it switched out the ROM, accessed RAM, switched the ROMs in again and returned.
After all, that same scheme was intended for the C64 as well, but by Tramiel's decision it was produced the way the prototypes where hacked together, not waiting for BASIC 3.5 to finish.
Another advantage of the TED line was to have the I/O area moved to the top 4 KiB at $FC00, thus resulting in a continous RAM Block of 60 KiB when ROM was switched off (*4).
Detailed workings of the access stubs:
BASIC 3.5 was build in a very versatil and comfortable fashion. To do so the whole first two KiB, not just page zero, was used for variables and pointers (*5). Among others, page 4 contained separate stubs for all BASIC related pointers to fetch from. For example a stub called INDTXT at $04A5 to fetch BASIC program text.
INDTXT:
SEI * no interrupt
STA RAM_ON * disable ROM ($FF3F)
LDA (TXTPTR),Y * fetch byte (<patch>)
STA ROM_ON * enable ROM ($FF3E)
CLI * free IRQ
RTS * return the byte
All the stubs are copied from the same ROM code and patched at <patch> with the address of the corresponding pointer.
Similar the CHRGET/CHRGOT combo, central for Commodore BASIC and others get moved there - all including the ROM on/off switching.
*1 - In addition to the Kernal banking services like the 'Long Fetch' routine located at $FC7F (via Kernal entry point $FCF7) to access any of the 4 32 KiB ROM Banks.
*2 - No need to go thru such a stub for writing, as writes always end up in RAM.
*3 - Strictly this would apply only to addresses above $8000. No switching needed below that, as there is always RAM. Thus a simple (BIT) test for negative of the high byte (even free when loading(moving that pointer) can detect the need for using the stub. Something that always fails on a 16 KiB C16. Except, it hasn't been done as the savings are rather marginal - 8 clocks per fetch. There may be other areas to improve instead.
*4 - The C64 had a it/s 4 KiB I/O area at $D000, somewhat in the middle, cutting off the to 8 KiB, even if ROMs were disabled.
*5 - The November 1984 issue of the US magazine Commodore Computers contains a rather expressive address list.