Wednesday, May 27, 2015

How to Add a Function to the Ca65 Assembler


I've been extending ca65 to do a bit more. See my branch here: https://github.com/Movax12/cc65

For my own use I decided I should post some quick information on how to add a custom pseudo function to ca65. Adding a new control command is a bit different, but very similar - I may document that too at a later time.

For now, how to add a function:

You are going to need to edit the following files from the source:

token.h
scanner.c
pseudo.c
expr.c

First, think of a name for you function. I'm going to add the ".ismacro()" function. This function will check if an identifier is a macro or not, and return 1 if true, 0 if false.

First, let's add the token. Open "token.h"

Find the line in the source (around line 55):
typedef enum token_t {

Then find the comment:
 
    /* The next ones are tokens for the pseudo instructions. Keep together! */

Then we'll add the token for our function to the list, between TOK_INTERRUPTOR and TOK_LEFT (The list is in alphabetic order.)
...
TOK_INTERRUPTOR,
TOK_ISMACRO,
TOK_LEFT,
...

Save "token.h"

Now we have to associate that token with a 'dot keyword' to match the user source file to the token.
Open "scanner.c".

Find the array in the source (around line 134):
DotKeywords [] = {


And find a suitable spot to insert our new function:
...
{ ".INTERRUPTOR",   TOK_INTERRUPTOR },
{ ".ISMACRO",       TOK_ISMACRO     },
{ ".LEFT",          TOK_LEFT        },
...

Save "scanner.c".

Open "pseudo.c" and add the new function to the table there:

...
{ ccNone,           DoInterruptor   },
{ ccNone,           DoUnexpected    },      /* .ISMACRO */
{ ccNone,           DoInvalid       },      /* .LEFT */
...

This table is similar to the table in "token.h". This entry must be in the same place you entered it in "token.h". The 'DoUnexpected' means that this will output an error if found on a line by itself. This is a function and should be part of an expression.

Save "pseudo.c"

Now the part where we actually have to tell ca65 what to do with the new function. Open "expr.c"

What is ".ismacro" similar to that exists already in ca65? Something that takes the current token and evaluates it as an identifier.

Looking at existing code, and trying to fit with what is there, I've come up with:

static ExprNode* FuncIsMacro(void)
/* Handle the .ISMACRO builtin function */
{
    Macro* Mac = 0;

    /* Check for a macro or an instruction depending on UbiquitousIdents */

    if (CurTok.Tok == TOK_IDENT) {
        Mac = FindMacro(&CurTok.SVal);
    } 
    else
    {
        Error("Identifier expected." );
    }
    /* Skip it */
    NextTok();
    return GenLiteralExpr(Mac != 0);
}

Add this code to a suitable location in "expr.c".  Now we just need to add a call to the above code to the function:

static ExprNode* Factor (void)

Find somewhere in the switch to add this:

  case TOK_ISMACRO:
       N = Function(FuncIsMacro);      
       break;

Save that file, and build your binary. Done. Your new function should work.

Extra: How to set a new feature to be part of the .feature command.

Open "global.h". Add a new line for your feature:

extern unsigned char IsMacro;            /* Allow .ISMACRO function */

Save. Open "global.c". Add a new line for your feature:

unsigned char IsMacro = 0;   /* Allow .ISMACRO function */

Save.

Now, to disable your new function by default, open "scanner.c" and add a new switch in the function "FindDotKeyword" to 'unmatch' the function:

static token_t FindDotKeyword (void)
/* Find the dot keyword in SVal. Return the corresponding token if found,
** return TOK_NONE if not found.
*/
{
    struct DotKeyword K;
    struct DotKeyword* R;

    /* Initialize K */
    K.Key = SB_GetConstBuf (&CurTok.SVal);
    K.Tok = 0;

    /* If we aren't in ignore case mode, we have to uppercase the keyword */
    if (!IgnoreCase) {
        UpcaseSVal ();
    }

    /* Search for the keyword */
    R = bsearch (&K, DotKeywords, sizeof (DotKeywords) / sizeof (DotKeywords [0]),
                 sizeof (DotKeywords [0]), CmpDotKeyword);
    if (R != 0) {

        /* ADDED:        */

        /* By default, disable any DotKeyword that has been added and not accepted to the main branch. */

        switch (R->Tok) {

            case TOK_ISMACRO:
                /* Disallow .ISMACRO function */
                if (IsMacro == 0) {
                    return TOK_NONE;
                }
                break;

            default: /* Keep GCC quiet */ break;
        }
        /* END ADDED      */
        return R->Tok;
    } else {
        return TOK_NONE;
    }
}


Also alter "feature.h" and "feature.c" to allow turning on the feature. This should be obvious by looking at the existing code.

That's it!