If you restrict your use of BASIC to statements and commands and functions that exist in version 2.0, with no memory-address references (so no
WAIT), and keep the program small enough to fit into the unexpanded VIC-20's 3583 bytes (including space taken up by variable values during the course of the program's execution), you can write a program that will load and run on any of the 8-bit Commodore machines. Everyone's favorite maze generator works great on all of them, from an original PET all the way through to a C65:
10 PRINT CHR$(142);:REM SHIFT TO UC/GFX, NOT THE DEFAULT ON ALL MACHINES
20 PRINT CHR$(205.5+RND(0));:GOTO 20
So with some care you can write portable BASIC. Portable machine code is harder; you run into a problem right away just picking a load address. Most machine-code programs on Commodores were either on cartridges (which appeared at a fixed spot in the address space) or loaded into the BASIC program area, in which case they started with a small BASIC program that used
SYS to launch the machine code. But that's still machine-dependent; the most common start address for BASIC text on these machines is 4096/$1000, which is where it is on the PET, unexpanded VIC-20, and C-16/Plus4. But as soon as the VIC-20 has any memory expansion, the start of BASIC moves down to to 1024/$0400. BASIC text in the C-64 starts at 2048/$0800, while on the C-128 it starts at 7168/$1C00 - unless the high-resolution graphics screen has been reserved, in which case it starts at 16384/$4000 (and is hidden behind BASIC ROM, so you have to change the memory
BANK to call it)! And even if you can reliably get the program loaded and executed, it's very hard to write position-independent 6502 code.
You could no doubt write some 6502 assembly source that would work across all the machines as long as it was reassembled (or at least re-loaded/linked, depending on how sophisticated your build system is) for each target machine. And there are of course some ways for code to inspect the machine and figure out which model it's being run on, so you can have different versions of some of the logic for different ones. Probably not and still fit into an unexpanded VIC-20, though. :)
And you'd be pretty much restricted to text I/O. The C64, C128, and C65 are the only machines with sprites; the TED machines (C-16 and Plus/4) do have high-res graphics capability, but while they offer the same basic graphics modes as the VIC-II machines (C64 and C128), the details are completely different. The C-65 uses a VIC-III and is different yet again.
The post-PET machines do all have the ability to simulate high-res graphics using custom character sets, though an unexpanded VIC only lets you map a small patch of screen that way. Even so, the details of how you go about setting that up are completely different from video chip to video chip, so you'd be looking at a lot of platform-conditional code.