[Project Log] Python on the 6502/C64, 8080, 6800, 6809 and AVR

Because it’s not there…

I took on learning Python because it appeared to be a language with demand. Then came realization that it is so popular because it is today’s BASIC. For good and bad. Implementing some of the Pythonics was an irresistible challenge.

5 Likes

Because its better than running Node.js or even Lua on IoT/embedded devices. Seriously, have you tried nodemcu? A little bloated imho. Sure lua is great but python is an everyday language (just as Basic and TCL/Tk was) while javascript is a different beast all together.

6 posts were split to a new topic: Embedded Language Wars

The 6502 instruction set is kind of spare when you compare it with, for example, the 6809 (or even the lesser 6800, Z80, and 8080). However, even when you have to use several instructions in place of a single 6809 instruction, the number of cycles ends up being comparable. The code density is what suffers, and that’s because the 6502 is intentionally RISC. For many small embedded applications, the code density is comparable and the 6502 uses many fewer cycles.

A good macro assembler can make the 6502 much less tedious and can improve readability.

We did get an Amiga 1200 in and that is hooked up right now but the c128 will still be around for your use and we’ll have to find a way to secure it on the wall while not in use.

2 Likes

Actually, the 6800 and 6809 are very similar to the 6502 in that they take one machine cycle to do a memory read or write or ALU operation, unlike the 8080 or Z80 which takes around 3 T cycles. If you can get it into the X register, the 680x can do a 16-bit increment or decrement in 4 machine cycles. The 680x has two capable accumulators. The advantage the 6502 has is that it pipelines the fetch of the next instruction.

My assembler does macros. But there are many flavors of operations: zeropage or absolute addressing versus indirect using register Y, simple increment or decrement versus adding or subtracting a constant or another variable. Then there are standalone operations versus ones to be done in a sequence where a little of gain can be had by keeping a byte in the X register. If I create macros to cover every case, it will begin looking like the ARM with its conditional execution and optional updating of condition codes.

This is sort of apples and oranges, but this is a block copy subroutine for the 6502:

 0344 A0 00	      [2] 00498	MovBlk	ldy	#0			; Start of first page
 			  00499
 0346 A6 07	      [3] 00500		ldx	Int0+1			; At least one full page remaining?
 0348 F0 07 (0351)  [2/3] 00501		beq	MovBlk0			; Branch if no
 			  00502
 034A A2 00	      [2] 00503		ldx	#0			; Set up to move an entire page
 034C C6 07	      [5] 00504		dec	Int0+1			; One fewer whole page remaining
 			  00505
 034E 4C 0357	      [3] 00506		jmp	MovBlk1
 			  00507
 0351 A6 06	      [3] 00508	MovBlk0	ldx	Int0			; Any remaining on a partial page?
 0353 F0 0F (0364)  [2/3] 00509		beq	MovBlk2			; No
 			  00510
 0355 84 06	      [3] 00511		sty	Int0			; This will finish the partial page
 			  00512
 0357 B1 10	    [5/6] 00513	MovBlk1	lda	(Ptr0),Y		; Move a byte
 0359 91 12	      [6] 00514		sta	(Ptr1),Y
 035B C8	      [2] 00515		iny
 035C CA	      [2] 00516		dex				; More to move on this page?
 035D D0 F8 (0357)  [2/3] 00517		bne	MovBlk1			; Yes
 			  00518
 035F E6 11	      [5] 00519		inc	Ptr0+1			; Address next page
 			  00520
 0361 4C 0344	      [3] 00521		jmp	MovBlk			; Check for another page
 			  00522
 0364 60	      [6] 00523	MovBlk2	rts

While this is the one for the 6800:

 0171 B6 016F	      [4] 00213	Copy_    ldaa   CopyC_
 0174 BA 0170	      [4] 00214	         oraa   CopyC_+1
 0177 27 21 (019A)    [4] 00215	         beq    Copy2_
 0179 FE 016B	      [5] 00216	Copy1_   ldx    CopyS_
 017C A6 00	      [5] 00217	         ldaa   ,X
 017E 08	      [4] 00218	         inx
 017F FF 016B	      [6] 00219	         stx    CopyS_
 0182 FE 016D	      [5] 00220	         ldx    CopyD_
 0185 A7 00	      [6] 00221	         staa   ,X
 0187 08	      [4] 00222	         inx
 0188 FF 016D	      [6] 00223	         stx    CopyD_
 018B 7A 0170	      [6] 00224	         dec    CopyC_+1
 018E 26 E9 (0179)    [4] 00225	         bne    Copy1_
 0190 7D 016F	      [6] 00226	         tst    CopyC_
 0193 27 05 (019A)    [4] 00227	         beq    Copy2_
 0195 7A 016F	      [6] 00228	         dec    CopyC_
 0198 26 DF (0179)    [4] 00229	         bne    Copy1_
 019A 39	      [5] 00230	Copy2_   rts

The 6800 code does not use variables in the direct page; if it did, it would take one fewer cycle for each instruction which read or wrote a variable (Copy?_)

Edit: …and for the 8080:

 0000			  00001	BlkMov:
 0000 2A 0017	     [16] 00002		lhld	Src
 0003 EB	      [4] 00003		xchg
 0004 2A 0019	     [16] 00004		lhld	Count
 0007 44	      [5] 00005		mov	B,H
 0008 4D	      [5] 00006		mov	C,L
 0009 2A 0015	     [16] 00007		lhld	Dest
 			  00008
 000C			  00009	Loop:
 000C 1A	      [7] 00010		ldax	D
 000D 77	      [7] 00011		mov	M,A
 000E 23	      [5] 00012		inx	H
 000F 13	      [5] 00013		inx	D
 0010 0B	      [5] 00014		dcx	B
 0011 C2 000C	     [10] 00015		jnz	Loop
 			  00016
 0014 C9	     [10] 00017		ret

Further edit; and for the AVR, the controller on the arduino:

 000000 9610	      [2] 00005	Copy:	adiw	R26,0
 000001 F041=00000A [1/2] 00006		breq	Copy2
 000002 E161	      [1] 00007		ldi	R22,high(SRAM_START+SRAM_SIZE)
 000003 30E0	      [1] 00008		cpi	R30,low(SRAM_START+SRAM_SIZE)
 000004 07F6	      [1] 00009		cpc	R31,R22
 000005 F428=00000B [1/2] 00010		brcc	Copy3
 000006 9161	      [2] 00011	Copy1:	ld	R22,Z+
 000007 9369	      [2] 00012		st	Y+,R22
 000008 9711	      [2] 00013		sbiw	R26,1
 000009 F7E1=000006 [1/2] 00014		brne	Copy1
 00000A 9508	      [2] 00015	Copy2:	ret
 00000B 9165	      [3] 00016	Copy3:	lpm	R22,Z+
 00000C 9369	      [2] 00017		st	Y+,R22
 00000D 9711	      [2] 00018		sbiw	R26,1
 00000E F7E1=00000B [1/2] 00019		brne	Copy3
 00000F 9508	      [2] 00020		ret

Edit once again; and the the champion of the 8-bitters, the 6809;

    1   0000 FE   0015     Copy    ldu    Src		; 6 cycles
    2   0003 10BE 0017             ldy    Dest		; 7 cycles
    3   0007 BE   0019             ldx    Count		; 6 cycles
    4   000A 27   08               beq    Done		; 2 cycles
    5                      
    6   000C A6   C0       Loop    lda    ,U+		; 4+2 cycles
    7   000E A7   A0               sta    ,Y+		; 5+2 cycles
    8   0010 30   1F               leax   -1,X		; 4+1 cycles
    9   0012 26   F8               bne    Loop		; 2 cycles
   10                      
   11   0014 39            Done    rts			; 5 cycles

I do not have one for the 8080/Z80 handy, but it can use BC, DE and HL to indirectly access memory, so it would use a lot less memory access switching pointers.

Looking at the 6800 code now, I could replace

 018B 7A 0170	      [6] 00224	         dec    CopyC_+1
 018E 26 E9 (0179)    [4] 00225	         bne    Copy1_
 0190 7D 016F	      [6] 00226	         tst    CopyC_
 0193 27 05 (019A)    [4] 00227	         beq    Copy2_
 0195 7A 016F	      [6] 00228	         dec    CopyC_
 0198 26 DF (0179)    [4] 00229	         bne    Copy1_

with something like (not correct code, just similar instructions to what would be needed):

 0179 FE 016B	      [5] 00216	Copy1_   ldx    CopyS_
 017E 08	      [4] 00218	         inx
 017F FF 016B	      [6] 00219	         stx    CopyS_
 0198 26 DF (0179)    [4] 00229	         bne    Copy1_

Edit: looking back at it, I would not do the replacement. In 255 out of 256 cases, the branch at the second instruction would be taken back to the top of the loop after only 10 cycles.

Further edit: CopyC_+1 could be kept in the B register for a savings of 4 cycles each time around the inner loop.

Looking back at the block copy routines, the 6800 is the worst processor in this particular case. The lack of a second index register severely hurt it. I do not have a version of the code for the 6809, but it should do much better since it has two additional index registers.

The 6502 does very well as the zero page locations can be effectively used for indexing. The 8080 did OK because its register pairs can be used for indexing.

In terms of raw speed, the AVR shines. It has many registers and several of them have fancy autoincrement modes. But it has a very limited amount of static RAM, so a general purpose computer it is not.

Edit: It appears that the 6502, with its funky indirect indexed addressing mode, managed to beat the 6809 by 2 cycles in the inner loop of a block copy.

An interesting exercise for another day is to compare how each one does implementing the FORTH inner interpreter.

Many of these chips have as much or more then the 70’s and 80’s general purpose computers. Some as much as 256 kbytes of RAM.

Anyway, back to the Python compiler. I go from hard to very hard. The print function is done except for handling the keyword arguments.

In Python, variables are just a reference to an object. Essentially nothing but a pointer. A variable can reference an object of any type; a boolean, an integer, a real number, a string or even a function. The run-time code must determine the type and do something reasonable with it.

The bad part of it, as least for the compiler implementer, is that nothing is known at compile time about the number and types of the arguments to a function. Adding to the complexity is that you can have traditional positional arguments, and then some keyword arguments and they can be assigned a default value if the function call does not specify one.

Languages like Pascal have been criticized because parts of the language are “special.” Write and writeln cannot be replaced or extended without modifying the compiler. Python, at least Python 3, does not have that problem. I can essentially write:

def myPrint(*pargs, **kargs):
        print(*pargs, **kargs)

and alter the way the print function works. I can even do

        oldPrint = print
        print = myPrint

and replace the provided one with mine.

But that power comes with a heavy price. Especially with Python’s dynamic typing. The burden is on the run-time code to figure out how to map the arguments when a function is called.

The AVR does not. I do not know whether the larger MSP430s do. They are also Harvard architecture, making them less suitable for general purpose use loading arbitrary application code from a storage device. The ARM is the notable exception to these limitations and why it is so popular today.

Your correct, I just looked at the data sheet and they seem to max out at 16K of RAM, which is still more then enough to run a general purpose computer, and with some of the larger pin devices it wouldn’t be hard to add more external RAM that could be paged in/out of the chip.

It certainly wouldn’t be a viable design, but it is doable, and would be an interesting exercise in retro inspired computer design. If UNIX could be developed on a PDP/7, it would certainly be possible to create a version for the AVR…

I remembered this

So it would be possible to implement an AVR on an FPGA with as many resources as one wants… Can’t you see it, and AVR with a PDP/11 front panel running UNIX and supporting the native debugging of Arduino and Assembly code?

Yep and it is possible to programmatically change your flash on the AVR with a running program. So a simple monitor like program could load from and SD, or through the serial port and place the code in flash and allow you to run it. Haven’t looked at AVR assembly and don’t remember if there is any where for the code to single step…

There is a way for the code to write to flash memory since the bootloader does exactly that. I never learned to do that, but it was something I will eventually have to do to really finish my FORTH implementation for the AVR.

I do not remember there being a single step bit or interrupt like the x86. You may have to add some external hardware to yank an NMI to do that.

I originally wrote this on the x86 16-bit following the roadmap in the Byte book on Threaded Interpretive Languages:

    656				     ;-----------------------------------------------------------------------------
    657				     ;
    658				     ; Inner interpreter implementation
    659				     ;
    660	00E5			     CODE    ends
    661	0000			     DICT    segment
    662
    663				     ;
    664				     ; SEMI does not have a header, but	has a standard word address
    665				     ;
    666	0000  00E5r		     _SEMI   dw	     offset SEMI
Turbo Assembler	 Version 3.2	    05/06/15 15:45:56	    Page 10
tilli.ASM
TILLI.ASM - Threaded Interpretive Language Little Implementation


    667
    668	0002			     DICT    ends
    669	00E5			     CODE    segment
    670
    671	00E5			     SEMI:
    672				     public  SEMI
    673	00E5  8B 76 00			     mov     SI,[BP]		     ; Pop return address
    674	00E8  83 C5 02			     add     BP,2
    675
    676	00EB			     NEXT:
    677				     public  NEXT
    678	00EB  8B 3C			     mov     DI,[SI]		     ; Get next	word address
    679	00ED  83 C6 02			     add     SI,2
    680
    681	00F0			     RUN:
    682				     public  RUN
    683	00F0  8B 1D			     mov     BX,[DI]		     ; Run a threaded word
    684	00F2  83 C7 02			     add     DI,2
    685	00F5  FF E3			     jmp     BX
    686
    687	00F7			     __COLON:
    688				     public  __COLON
    689	00F7  83 ED 02			     sub     BP,2		     ; Push instruction	register
    690	00FA  89 76 00			     mov     [BP],SI
    691	00FD  8B F7			     mov     SI,DI		     ; Point to	nested secondary
    692	00FF  EB EA			     jmp     short NEXT
    693
    694
    695				     ;-----------------------------------------------------------------------------
    696				     ;
    697				     ; EXECUTE ( addr -- )
    698				     ;
    699				     ; Execute dictionary entry	at compilation address on stack; for example,
    700				     ; address returned	by FIND.
    701				     ;
    702	0101			     CODE    ends
    703	0002			     DICT    segment
    704
    705					     header  <'EXECUTE'>
1   706	0002  0000			     dw	     PREV_ENTRY
1   707	0004  07			     db	     offset ??0007 - offset $ -	1    ; the 'EXECUTE' length
1   708	0005  45 58 45 43 55 54	45	     db	     'EXECUTE'
1   709	000C			     ??0007  label   byte
    710
    711	000C  0101r		     _EXECUTE	     dw	     offset __EXECUTE
    712
    713	000E			     DICT    ends
    714	0101			     CODE    segment
    715
    716	0101			     __EXECUTE:
    717	0101  5F			     pop     DI			     ; Get word	address	of the word
    718	0102  EB EC			     jmp     Run

From there, I ported it to the 6800;

 			  00532	;=== < Inner interpreter >====================================================
 			  00533
 			  00534	;-----------------------------------------------------------------------------
 			  00535	;
 			  00536	; SEMI does not have a header, but has a standard word address
 			  00537	;
 016E 0170		  00538	_SEMI    fdb    SEMI
 			  00539
 0170			  00540	SEMI:
 0170 DE 00	      [4] 00541	         ldx    RS        ; Pop IR from return stack
 0172 08	      [4] 00542	         inx
 0173 08	      [4] 00543	         inx
 0174 DF 00	      [5] 00544	         stx    RS
 0176 EE 00	      [6] 00545	         ldx    ,X
 0178 DF 02	      [5] 00546	         stx    IR
 			  00547	;	mov	SI,[BP]			; Pop return address
 			  00548	;	add	BP,2
 			  00549
 017A			  00550	Next:
 017A DE 02	      [4] 00551	         ldx    IR
 017C 08	      [4] 00552	Next1    inx
 017D 08	      [4] 00553	         inx
 017E DF 02	      [5] 00554	         stx    IR
 0180 EE 00	      [6] 00555	         ldx    ,X
 			  00556	;	mov	DI,[SI]			; Get next word address
 			  00557	;	add	SI,2
 			  00558
 0182			  00559	RUN:                      ; WA in X
 0182 DF 04	      [5] 00560	         stx    WA        ; Run machine code of new word
DEI Research 6800 Cross Assembler  Version 0.0   05-16-2015 112:47:26  Page 11
tilli.a68

 Addr Code	   Cycles Line#	  Source Statement

 0184 EE 00	      [6] 00561	Run1     ldx    ,X
 0186 6E 00	      [4] 00562	         jmp    ,X
 			  00563	;	mov	BX,[DI]			; Run a threaded word
 			  00564	;	add	DI,2
 			  00565	;	jmp	BX
 			  00566
 0188			  00567	__COLON:
 0188 DE 00	      [4] 00568	         ldx    RS        ; Push instruction register
 018A D6 03	      [3] 00569	         ldab   IR+1      ; on return stack
 018C 09	      [4] 00570	         dex
 018D E7 02	      [6] 00571	         stab   2,X
 018F 96 02	      [3] 00572	         ldaa   IR
 0191 09	      [4] 00573	         dex
 0192 A7 02	      [6] 00574	         staa   2,X
 0194 DF 00	      [5] 00575	         stx    RS
 			  00576
 0196 DE 04	      [4] 00577	         ldx    WA        ; Execute new secondary
 0198 20 E2 (017C)    [4] 00578	         bra    Next1
 			  00579	;	sub	BP,2			; Push instruction register
 			  00580	;	mov	[BP],SI
 			  00581	;	mov	SI,DI			; Point to nested secondary
 			  00582	;	jmp	short NEXT

And finally the AVR:

 			  00572	;=== < Inner interpreter >====================================================
 			  00573
 			  00574	;-----------------------------------------------------------------------------
 			  00575	;
 			  00576	; SEMI does not have a header, but has a standard word address
 			  00577	;
 			  00578	.init
 0146 0102		  00579	?SEMI:	.dw	SEMI
 			  00580	.cseg
 			  00581
 000102 01F7	      [1] 00582	SEMI:	movw	R30,R14			; Pop return address
 000103 91A1	      [2] 00583		ld	R26,Z+
 000104 91B1	      [2] 00584		ld	R27,Z+
 000105 017F	      [1] 00585		movw	R14,R30
 			  00586	;	mov	SI,[BP]
 			  00587	;	add	BP,2
 			  00588
 000106 91CD	      [2] 00589	Next:	ld	R28,X+			; Get next word address in secondary
 000107 91DD	      [2] 00590		ld	R29,X+
 			  00591	;	mov	DI,[SI]
 			  00592	;	add	SI,2
 			  00593
 000108 91E9	      [2] 00594	Run:	ld	R30,Y+			; Run the word
 000109 91F9	      [2] 00595		ld	R31,Y+
 00010A 9409	      [2] 00596		ijmp
 			  00597	;	mov	BX,[DI]
 			  00598	;	add	DI,2
 			  00599	;	jmp	BX
 			  00600
 00010B			  00601	?COLON:
 00010B 01F7	      [1] 00602		movw	R30,R14			; Push instruction register
 00010C 93B2	      [2] 00603		st	-Z,R27
 00010D 93A2	      [2] 00604		st	-Z,R26
 00010E 017F	      [1] 00605		movw	R14,R30
 00010F 01DE	      [1] 00606		movw	R26,R28			; Point to nested secondary
 000110 CFF5=000106   [2] 00607		rjmp	Next			; And run it
 			  00608	;	sub	BP,2
 			  00609	;	mov	[BP],SI
 			  00610	;	mov	SI,DI
 			  00611	;	jmp	short Next
 			  00612
 			  00613	;-----------------------------------------------------------------------------
 			  00614	;
 			  00615	; EXECUTE ( addr -- )
 			  00616	;
 			  00617	; Execute dictionary entry at compilation address on stack; for example,
 			  00618	; address returned by FIND.
 			  00619	;
 			  00620	.init
 			  00621	header	"EXECUTE",0
+         =00000148		 .set	THIS_ENTRY	= PC
+0148 0138			 .dw	PREV_ENTRY
+         =00000148		 .set	PREV_ENTRY	= THIS_ENTRY
+014A 0745584543555445		 .db	0 | strlen("EXECUTE"),"EXECUTE"
DEI Research AVR Cross Assembler  Version 0.0   Jun-23-2015 57:32:32  Page 12
tilli.ASM

  Addr   Code	   Cycles Line#	  Source Statement

 0152 0111		  00622	_EXECUTE:	.dw	__EXECUTE
 			  00623	.cseg
 			  00624
 000111			  00625	__EXECUTE:
 000111 91CF	      [2] 00626		pop	R28			; Get word address of the new word
 000112 91DF	      [2] 00627		pop	R29
 000113 CFF4=000108   [2] 00628		rjmp	Run
 			  00629	;	pop	DI
 			  00630	;	jmp	Run

This project had been set aside while I did other things until a friend asked about it, giving me a kick in the butt to work on it again.

The lexical analysis handling of indentation is finally done. If you know Python at all, this is a key part of the language.

Next up is implementing the symbol table and literal pool, along with a simplistic expression parser. Once those are done, I will be able to compile simple Python programs for my 6502 virtual machine.

Other things to do in the near-term:

  • Modify the run-time library code and the assembler to generate code for the Commodore 128.
  • Python integers grow to fit the data. The run-time library can handle this; the compiler currently cannot.
  • Implement more language features.
  • Modify the assembler to add an option to be case-sensitive.
  • Python does not set a limit to the length of variable names. The current tools have a limit of 32 characters.
  • Python does not set a limit on the length of a line of source code. The current tools have a limit of 255 characters.
  • Port back end to support another platform in addition to the 6502. 68000? x86? AVR (Arduino?)
4 Likes

The following Python code:

a = b = c

generates this 6502 code:

 020E AE 13A7	      [4] 00093		ldx	_c
 0211 8A	      [2] 00094		txa
 0212 0D 13A8	      [4] 00095		ora	_c+1
 0215 D0 0B (0222)  [2/3] 00096		bne	L00000
 			  00097
 0217 A9 A7	      [2] 00098		lda	#_c&$FF
 0219 85 36	      [3] 00099		sta	Var
 021B A9 13	      [2] 00100		lda	#_c>>8
 021D 85 37	      [3] 00101		sta	Var+1
 021F 20 1344	      [6] 00102		jsr	Undef
 			  00103
 0222			  00104	L00000
 0222 AD 13A8	      [4] 00105		lda	_c+1
 0225 8E 139D	      [4] 00106		stx	_a
 0228 8D 139E	      [4] 00107		sta	_a+1
 022B 8E 13A2	      [4] 00108		stx	_b
 022E 8D 13A3	      [4] 00109		sta	_b+1
 			  00110
 0231 86 10	      [3] 00111		stx	Ptr0
 0233 85 11	      [3] 00112		sta	Ptr0+1
 0235 20 04FF	      [6] 00113		jsr	AddRef
 0238 20 04FF	      [6] 00114		jsr	AddRef

The first block of code checks whether the variable c is defined.

The second block of code displays an error message if not.

The third block of code assigns the reference to c to variables a and b.

The fourth block of code increments the reference count.

As I have been anticipating, dynamic typing imposes quite a bit of overhead.

1 Like

From the Things are Not as Easy as They Appear Department:

The previous code snippet had a serious flaw: previous object references by variables a and b were not decremented.

So, the following Python code:

a = b = c

generates this 6502 code:

 020E AE 14D2	      [4] 00094		ldx	_c
 0211 8A	      [2] 00095		txa
 0212 0D 14D3	      [4] 00096		ora	_c+1
 0215 D0 0B (0222)  [2/3] 00097		bne	L00000
 			  00098
 0217 A9 D2	      [2] 00099		lda	#_c&$FF
 0219 85 38	      [3] 00100		sta	Var
 021B A9 14	      [2] 00101		lda	#_c>>8
 021D 85 39	      [3] 00102		sta	Var+1
 021F 20 146F	      [6] 00103		jsr	Undef
 			  00104
 0222			  00105	L00000
 0222 AD 14D3	      [4] 00106		lda	_c+1
 0225 86 10	      [3] 00107		stx	PtrA
 0227 85 11	      [3] 00108		sta	PtrA+1
 0229 20 062A	      [6] 00109		jsr	AddRef
 022C 20 062A	      [6] 00110		jsr	AddRef
 			  00111
 022F AE 14C8	      [4] 00112		ldx	_a
 0232 AD 14C9	      [4] 00113		lda	_a+1
 0235 86 12	      [3] 00114		stx	Ptr0
 0237 85 13	      [3] 00115		sta	Ptr0+1
 0239 20 064F	      [6] 00116		jsr	DeRef
 023C AE 14CD	      [4] 00117		ldx	_b
 023F AD 14CE	      [4] 00118		lda	_b+1
 0242 86 12	      [3] 00119		stx	Ptr0
 0244 85 13	      [3] 00120		sta	Ptr0+1
 0246 20 064F	      [6] 00121		jsr	DeRef
 			  00122
 0249 A6 10	      [3] 00123		ldx	PtrA
 024B A5 11	      [3] 00124		lda	PtrA+1
 024D 8E 14C8	      [4] 00125		stx	_a
 0250 8D 14C9	      [4] 00126		sta	_a+1
 0253 8E 14CD	      [4] 00127		stx	_b
 0256 8D 14CE	      [4] 00128		sta	_b+1

The first block of code checks whether the variable c is defined.

The second block of code displays an error message if not.

The third block of code increments the reference count.

The fourth block of code decrements the reference counts for a and b referenced objects.

The fifth block of code assigns the reference to c to variables a and b.