Most of the tass example programs start with a header like this, which places BASIC code at the beginning of the file that has an appropriate SYS instruction to run the rest of the application:

*       = $0801
        .word (+), 2005  ;pointer, line number
        .null $9e, format("%d", start);will be sys 4096
+   .word 0          ;basic line end

*       = $1000

start   rts

I would like to reproduce this in ca65 compatible syntax, and the one line that is given me trouble is:

.null $9e, format("%d", start)

There is a .sprintf function in ca65, but it appears to only work with constant values. That is, if I have:

.ORG    $0801
        .word :+, 10            ; pointer, line number
        .byte $9e, (.sprintf("%d", start))
                                ; this is `start` as a decimal string
        .byte 0                 ; string terminator
        .word 0

start:  rts

I get:

example.s(8): Error: Constant expression expected
example.s(8): Error: Syntax error

While obviously one could simply hardcode the start address, is there any way to create a string value containing the decimal version of a memory address?

  • What does .word start give you? Jan 27 '20 at 7:53
  • Could you not make start a constant, since you know it's going to be 4096?
    – JeremyP
    Jan 27 '20 at 8:54

ca65 was designed as a single-pass assembler. So this works:

addr    =       4096
        .byte   $9e,.sprintf("%d", addr)

but this does not:

        .byte   $9e,.sprintf("%d", addr)
addr    =       4096
--> Error: Constant expression expected

The expression can't simply be constant; it must be constant and has to be defined before the sprintf function is encountered. If the assembler knows the size of the output (e.g. .word addr requires two bytes) it can reserve the space and fill it in later, but in this case the output is variable width.

If you're actually forcing the address to $1000 as in your 64tass example, you can just define a constant for the address above the sprintf. If you want the code to flow to the next address, you can't do it easily because the assembler simply doesn't know how large the formatted value is when the sprintf function is evaluated.

As you noted, the simple approach is to just hard-code the header, which doesn't really change for simple programs:

         .org    $0801
         .word   $0801      ;PRG header
         .org    $0801
         .word   next       ;ptr to next line
         .word   $0014      ;arbitrary line num
         .byte   $9e        ;SYS 2061 ($080d)
         .byte   "2061"
         .byte   $00
next:    .word   $0000

start:   nop                ;$80d

There's no simple way around this because of the assembler's design.


this fixes the decimals in the sys to 5 10 SYS00000 (and as such the string now has a fixed length). wastes 1 byte but then again a space also wastes 1 byte and that's common enough in stubs.

guess you can add "IF BASICSTART+STUBSIZE >= 10 /100/1000/10000 decimal" changes to the added values to cover for all eventualities along with some extra .ASCIIZ .sprintf() lines (or simply remove the "05" from the "d" formatting as all you need to 'adjust' are the offsets anyway) as assembler macros. example for c64 but should work on all ms-basic running cbm machines and derivatives thereof by changing BASICRAM to whatever that machine expects.

BASICRAM=$0801                  ; 0801 = C64
.ORG $0801
.WORD $000A             ; LINE NUMBER
.BYTE $9E               ; BASIC SYS COMMAND
.ASCIIZ .sprintf("%05d",*+$08)
.WORD $0000             ; END OF BASIC (NEXT POINTER $0000)

 LDA #$00
 STA $D020
  • Nice, except it still uses a constant length, not solving the original task to calculate the distance based on the START label.
    – Raffzahn
    Jan 30 '20 at 15:32
  • would that 'start label' have any arbitrary basic code in front of it... or just SYS with varying decimal address lengths. as it can be adjusted to do -that-... entire basic programs however are gonna be a different story. changing the stub is mostly done to handle the pet8032, vic20 and other cbm machines with different basic start addresses. Jan 30 '20 at 15:40
  • btw the * variable is correct and labels after a .asciiz() are therefore collected correctly... so it actually -should- be possible as labels can be added and substracted and multiplied (in 64 bit even ;) just as well. the length of the sys ascii-encoded-decimals just has to be calculated seperately i think.. Jan 30 '20 at 15:48
  • I'd say it can be as well assembly code or data, couldn't it? The basic Idea of the OP, as I understand it (sans the typo) is to have the assembler calculate whatever is distance is to be covered, no matter what's inbetween. Like shown in the TASS example. Isn't it? The last paragraph of the question does mention that he already thought about of hardcoding it.
    – Raffzahn
    Jan 30 '20 at 15:49
  • No, labels can not be used for calculation. '*' is treated different. Try it. They again result in a non constant expression, thus unable to be used in sprintf CA65 does differ in that from the usual bunch of assemblers.
    – Raffzahn
    Jan 30 '20 at 15:54

There is a .sprintf function in ca65, but it appears to only work with constant values.

Excatly. That's why there's the first error (Error: Constant expression expected) occurs. Sprintf is evaluated right when the source is parsed. So it behaves much like a macro in C.

The second error (Syntax error) is due the parentheses around sprintf.

Try something like

    .byte $9e,.sprintf("%d", 99)

to see the workings in your listing (ca65 -l test.lst test.asm).

I assume you just copied from the manual page, but there the result is used in an expression for .ident, which can eat them. .byte will not.

Solving the original task will need some macro-magic. I'm not so sure if it's really worth it. So for now better stay with using a constant value.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.