ASMPROC finally released!

You need an actual VIC.

Moderator: Moderators

Post Reply
nippur72
de Lagash
Posts: 574
Joined: Thu Sep 07, 2006 8:35 am

ASMPROC finally released!

Post by nippur72 »

After more than 10 years, I'm finally releasing ASMPROC.

ASMPROC on GitHub.

ASMPROC is a super-set of the assembly language which adds structured programming (like IF ... THEN, FOR ... NEXT, etc), making it a lot easier to program in machine language. It works like a pre-processor: you feed it an asmproc-compliant source file and it will produce a .ASM file that you can compile with a separate assembler.

The story behind ASMPROC in brief is this: back in 2006 I discovered this wonderful forum and became interested in VIC-20 back again. I decided to write some ML game, but soon discovering I could not tolerate 6502's syntax: it was too verbose and too cryptic for me. So I started to write a small tool that would convert "IF THEN" into the equivalent "BNE, BEQ, BCS, BCC etc..." instructions. The trick worked quite well and I kept extending the tool with things I encountered in my day-to-day programming. In brief it became a complete language that I enjoyed very much.

And it proved to be useful because it allowed me to write the following games with little effort:
  • Tetris
  • Return To Fort Knox, which was preceded by a complete reverse engineering of the "Raid on Fort Knox" cartdrige
  • Gomoku, also cross compiled for C16 and C64
  • Pong, written in collaboration with @nbla000
  • Tetris Deluxe, written by @nbla000 on the base of the original Tetris
You might remember them if you have read the forum between 2006 and 2009.

The only problem in ASMPROC was that it was written in hurry and without a broad picture of the whole, in other words the source code sucked :? .It was written in C++ and parsing was done brutally with lots of "indexof" and "substring". A nightmare that hindered me from extending it further.

So the plan in the years that came was to rewrite it, perhaps in another language, and refine it making it something that others could use.

I did several attempts at rewriting it, but all failed.

Recently I became interested again in retrocomputing and in Z80, so I decided to give it one more last try. I unzipped my old C++ source code and in something like a weekend, I converted the 2500 lines of spaghetti code into a JavaScript equivalent (TypeScript actually). Now I can reason on the code and can improve it if required.

I'm going to add support for Z80 (z80asm assembler) and ca65 because ASMPROC was originally meant to work with DASM.

If you are curious about this project, have a look at the README on Github just to make an idea of what it can do.
User avatar
Mike
Herr VC
Posts: 4839
Joined: Wed Dec 01, 2004 1:57 pm
Location: Munich, Germany
Occupation: electrical engineer

Re: ASMPROC finally released!

Post by Mike »

nippur72 wrote:If you are curious about this project, have a look at the README on Github just to make an idea of what it can do.
Nice!

This is surely helpful to quite some people to relieve them of the nitty-gritty details of control structures in machine code. Even if this might come at the expense of some extra code (sometimes, the flags are used in non-standard ways which don't map too well to the classic control structures...).

Some remarks to the README:

o The statement separator, " : ", is actually a colon, not a semicolon, :)

o In Bitmap Character Co_n_stants, shouldn't that be "B" for border, "F" for foreground and "A" for aux. colour? Your choice looks rather permuted, ...

o (Floating Point "Costants" is also missing an "n" ;) ...), and

o the macro "ldx indirect, a" should be "TAY : LDA ({1}),Y : TAX".

Furthermore, the MOD operator should be accompanied by DIV, so low/high-byte separation can be done unambiguously instead of relying on the respective assembler's interpretation of "<" and ">".

Any chances you get to finalise the formula parser you also were working on? That could also be abstracted by supplying the different addresses of the arithmetic routines in the BASIC interpreter of VIC-20 or C64, etc.

Cheers,

Michael
nippur72
de Lagash
Posts: 574
Joined: Thu Sep 07, 2006 8:35 am

Re: ASMPROC finally released!

Post by nippur72 »

Mike,

thanks for the in-deep review of ASMPROC :)

I've fixed all the issues and typos you mentioned (the README dates back to an age where spellcheckers were not invented yet :) ).

If you have any other suggestions, they are welcome!

ASMPROC is still a raw product, it's not "polished" as I would like. Once I tried to make a true language out of it (with a parser, AST and all the rest) but it was a too big effort. But now that I've converted it in JavaScript I can keep on improving it slowly but constantly. On top of my TODO list is a test case coverage, because when I fix something there is always something else that stops working :-)

I am also making it work for the Z80, but the overall syntax doesn't fit as well as for the 6502.

I will add DIV as well as LOBYTE and HIBYTE to make them work across assemblers (it's unfortunate there is no standard).

As for the expression compiler, I made no progress since 2009, I only have your CBMFloat() routine working. (BTW I had to slightly modify it for JavaScript, and add a frexp() implementation which is missing in the JS Math library).

But I am still interested to discuss about the expression compiler (perhaps in a separate thread). I think I can set a plain-vanilla expression parser with a JavaScript library named "nearley.js" and compile it using the generated AST. The only problem is that I no longer remember all the details about the CBM float routines, the FAC and all the rest. Are there any examples so I can refresh my memory?
User avatar
Mike
Herr VC
Posts: 4839
Joined: Wed Dec 01, 2004 1:57 pm
Location: Munich, Germany
Occupation: electrical engineer

Re: ASMPROC finally released!

Post by Mike »

nippur72 wrote:[...] I no longer remember all the details about the CBM float routines, the FAC and all the rest. Are there any examples so I can refresh my memory?
Three of those that come to my mind:

- DIV()- and MOD()-functions/routines (not finished - but most basic arithmetic is there): Functions with Two Arguments - see bottom of thread.

- SQR() reimplemented with Heron's algorithm: in the thread "Using and improving Exbasic", and

- complex arithmetic as in the inner loop of my Mandelbrot zoomer: fast Mandelbrot fractal generator.

In the last example, the archive contains the file 'source.txt', which - unfortunately - isn't that much verbose. :oops: However, it's 'just' the inner loop in line 17 of the original program:

Code: Select all

FORI=1TON:X2=X*X:Y2=Y*Y:IFX2+Y2<4THENXY=X*Y:X=X2-Y2+R:Y=XY+XY+J:NEXT
that I translated to machine code. The resulting code takes two shortcuts: the comparison with 4 is done by comparing the exponent byte, likewise the doubling of XY (i.e. XY+XY resp. 2*XY) is done by incrementing the exponent byte by 1. The rest consists of direct calls of the arithmetic routines in the BASIC interpreter, and 'boilerplate' FOR..NEXT code with 16-bit-integers for I and N.
nippur72
de Lagash
Posts: 574
Joined: Thu Sep 07, 2006 8:35 am

Re: ASMPROC finally released!

Post by nippur72 »

Good thanks!

I've also found the old project of mine where I had coded all floating point ops as assembler macros -- it needs to be revisited but it's a good starting point.

Still it's not clear to me if I can call ROM routines from machine language without side effects. For example suppose I want to calculate SIN(0.5), can I put 0.5 into FAC and jsr to SIN in the ROM? Can you provide an example such that?
User avatar
Mike
Herr VC
Posts: 4839
Joined: Wed Dec 01, 2004 1:57 pm
Location: Munich, Germany
Occupation: electrical engineer

Re: ASMPROC finally released!

Post by Mike »

nippur72 wrote:Still it's not clear to me if I can call ROM routines from machine language without side effects.
If you call a machine routine with SYS, everything that involves the zeropage regarding FP is finalized before control is handed over. You can't disturb the FP routines in that case (or the BASIC interpreter, for that matter).

Likewise with USR(), BASIC supplies a value and expects a result in FAC#1. No more, no less. Any state/node of the expression tree is kept on the CPU stack, that also means FP values already computed by anything in the expression to the left of USR() which are still "on the fly".

Of course, any calls of the FP routines will change FAC#1 and most also FAC#2 (or AFAC). Some of the more complex routines also use additional, temporary FACs ($4E..$52, $57..$5B, $5C..$60) - you will have to watch out for those if you call these routines *and* use the additional FACs also in your own code. In the DIV/MOD example, I stored one value away on the stack to allow for recursion.
For example suppose I want to calculate SIN(0.5), can I put 0.5 into FAC and jsr to SIN in the ROM? Can you provide an example such that?
Sure. Here:

Code: Select all

1 FORT=673TO695:READA:POKET,A:NEXT
2 DATA 169,179,160,2,32,162,219,32,104,226,162,184,160,2,32,212,219,96,128,0,0,0,0
3 FORT=0TO4:POKE696+T,0:NEXT
4 SYS673
5 FORT=0TO4:B=PEEK(696+T):GOSUB8:PRINTH$" ";:NEXT:PRINT
6 END
7 :
8 H$="":P=INT(B/16):GOSUB9:P=B-16*P
9 H$=H$+CHR$(48+P-7*(P>9)):RETURN

Code: Select all

.02A1  A9 B3     LDA #$B3
.02A3  A0 02     LDY #$02
.02A5  20 A2 DB  JSR $DBA2  ; load Y/A (i.e. $02B3 here) to FAC#1
.02A8  20 68 E2  JSR $E268  ; perform FAC#1 = SIN (FAC#1)
.02AB  A2 B8     LDX #$B8
.02AD  A0 02     LDY #$02
.02AF  20 D4 DB  JSR $DBD4  ; store FAC#1 to Y/X (i.e. $02B8 here)
.02B2  60        RTS

>02B3  80 00 00 00 00
... which results in $7F $75 $77 $43 $A2.

A quick check with a calculator (... $75 hides the MSB ...): $F57743A2 = 4118234018

4118234018 / 2^32 / 2 (to account for $7F as exponent) = 0.4794255385...

Conversely, SIN(0.5) = 0,4794255386... - so the result calculated by the machine code routine shows an expected error of around 1E-10. :)

Greetings,

Michael
nippur72
de Lagash
Posts: 574
Joined: Thu Sep 07, 2006 8:35 am

Re: ASMPROC finally released!

Post by nippur72 »

The following code calculates SIN(0.5) with ASMPROC today's version. Macros starting with "f" are floating point ops (fld = float load, ...).

Isn't that lovely how concise and compat it is?

Next question is: where to get a list of all ROM entry points (FSIN...)?

Code: Select all

   processor 6502
   org $1001

basic start
   10 print "SIN(0.5)=";
   20 sys {calculate}
basic end

NUMBER float 0.5

calculate:
   ld    ya, #NUMBER
   fld   fac, (ya)
   fsin
   fprint
   rts

macro fld "fac", "(ya)"
   jsr $dba2
end macro

macro fsin
   jsr $E268               
end macro

macro ld "ya", const
   lda #{2} MOD 256
   ldy #{2}/256
end macro

macro fprint 
   jsr $dddd
   lda #00
   ldy #01
   jsr $cb1e
end macro
nippur72
de Lagash
Posts: 574
Joined: Thu Sep 07, 2006 8:35 am

Re: ASMPROC finally released!

Post by nippur72 »

some more questions:
- what about logical operators? AND, OR, but also <, <= etc
- how to handle exceptions? e.g. SQRT(-1) does it RTS or gets lost somewhere in the ROM?
nippur72
de Lagash
Posts: 574
Joined: Thu Sep 07, 2006 8:35 am

Re: ASMPROC finally released!

Post by nippur72 »

I have a working embrional expression parser. Below is the output for "3*(4/1+sin(2))^3/4+pi".

I feed the input string to the parser which returns me a tree of nodes (aka AST). Then I visit the tree, emitting pseudo-instructions at each node.

The problem is that with this approach I'm forced to use a stack to handle expression nesting. Is the stack avoidable in some way? Perhaps evaluating the nodes in some order, I don't know, is that possible? What do you suggest?

Code: Select all

EXPRESSION:   
   fpush 3       
   fpush 4       
   fpush 1       
   fdiv          
   fpush 2       
   fsin          
   fadd          
   fpush 3       
   fexp          
   fmul          
   fpush 4       
   fdiv          
   fpush PI      
   fadd          
   rts
User avatar
Kweepa
Vic 20 Scientist
Posts: 1315
Joined: Fri Jan 04, 2008 5:11 pm
Location: Austin, Texas
Occupation: Game maker

Re: ASMPROC finally released!

Post by Kweepa »

I think it's possible to replace the stack with three registers, and have some functions to swap the values around where necessary.
In a lot of cases the expression only needs two registers. For example your expression could be evaluated like this:

floada 4
floadb 1
fdiv
fswap // swaps a and b
floada 2
fsin
fadd
floadb 3
fexp
floadb 3
fmul
floadb 4
fdiv
floada PI
fadd
rts

But for the general case it would need a one element stack, eg 1/2 + 3/4

floada 1
floadb 2
fdiv
fpush
floada 3
floadb 4
fdiv
fswap
fpop
fadd
rts
User avatar
Mike
Herr VC
Posts: 4839
Joined: Wed Dec 01, 2004 1:57 pm
Location: Munich, Germany
Occupation: electrical engineer

Re: ASMPROC finally released!

Post by Mike »

nippur72 wrote:Is the stack avoidable in some way?
Not in the general case when you allow expressions with binary operators and with parentheses.

That can be deduced by the simple fact the expression parser returns you a *tree*. Normally, each binary operator would pop its two arguments from the stack and then push its result. For evaluation enough, you rearrange the operators to work 'down' from the leaves. That at least eliminates the operator call stack. To minimize the data stack depth, you can then choose to evaluate the deepest branch of each node (beginning from the root) first. If one of the branches is a leaf, then it is directly available and doesn't need to be stacked. Only in the case the tree degenerates into a list, no stack is necessary at all.

For example:

(1 + 2 * 3) / (4 - (5 + 6) * 7)

Root: '/' - left branch: (1 + 2 * 3), right branch (4 - (5 + 6) * 7) - right branch is deepest: evaluate first.

Node: '-' - left branch: 4, right branch: (5 + 6) * 7 - right branch is deepest: evaluate first.

Node: '*' - left branch: 5 + 6, right branch: 7 - left branch is deepest: evaluate first.

Node: '+' - left branch: 5, right branch 6 - both branches are leaves, load both as arguments, add, leave result in FAC#1:

LOAD 5 -> FAC#2
LOAD 6 -> FAC#1
ADD

7 is leaf. Load as argument, multiply, leave result in FAC#1:

LOAD 7 -> FAC#2
MULT

4 is leaf. Load as argument, subtract (in the right order!), leave result in FAC#1:

LOAD 4 -> FAC#2
SUB (which does FAC#2 - FAC#1 -> FAC#1)

Now we're at the '/' root. Unfortunately, the left branch is not a leaf, so we need to stack the result of the right branch:

PUSH FAC#1

Node: '+' - left branch: 1, right branch 2 * 3 - right branch is deepest: evaluate first.

Note: '*' - left branch: 2, right branch 3 - both branches are leaves, load both as arguments, add, leave result in FAC#1:

LOAD 2 -> FAC#2
LOAD 3 -> FAC#1
MULT

1 is leaf. Load as argument, add, leave result in FAC#1:

LOAD 1 -> FAC#2
ADD

Finally, the root node, '/'. We need to move the numerator from FAC#1 to FAC#2, and then pop the denominator value from stack to FAC#1:

MOVE FAC#1 -> FAC#2
POP FAC#1 (from stack!)
DIV (FAC#2 / FAC#1 -> FAC#1)

... with the result in FAC#1. Stack depth: (only!) 1.
nippur72
de Lagash
Posts: 574
Joined: Thu Sep 07, 2006 8:35 am

Re: ASMPROC finally released!

Post by nippur72 »

Does evaluation of the deepest level come out with a recursive visit of the tree, right? I have a routine like this (it's JavaScript):

Code: Select all

function visittree(node: Node): string
{
   let out;
        if(node.type === "root")   { out = "EXPRESSION:\n" + visittree(node.arg) + "rts\n";   }
   else if(node.type === "parens") { out = visittree(node.arg); }
   else if(node.type === "^")      { out = visittree(node.base) + visittree(node.exponent) + "fexp\n"; }
   else if(node.type === "*")      { out = visittree(node.arg1) + visittree(node.arg2) + "fmul\n"; }
   else if(node.type === "/")      { out = visittree(node.arg1) + visittree(node.arg2) + "fdiv\n"; }
   else if(node.type === "+")      { out = visittree(node.arg1) + visittree(node.arg2) + "fadd\n"; }
   else if(node.type === "-")      { out = visittree(node.arg1) + visittree(node.arg2) + "fsub\n"; }
   else if(node.type === "sin")    { out = visittree(node.arg) + "fsin\n"; }
   else if(node.type === "pi")     { out = "fpush PI\n"; }
   else if(node.type === "number") { out = `fpush ${node.num}\n`; }
   else throw `unrecognized node ${(node as any).type}`;
   return out;
}
I think I can use the following tricks:
- if nodes are terminals (numbers), then I can load them directly (into FAC or AFAC)
- if nodes are non terminal I have to use "pop FAC" and "pop AFAC"
- evaluation of a node always results in "push FAC" unless it's the root node
- non commutative operations ("-", "/", "^") can be evaluated right branch first and left branch last, so that the last evaluation leaves the result in the FAC without needing to POP it.
- commutative ops can be argument-swapped if necessary, e.g. 3+(4/2) can be turned into (4/2)+3:

Code: Select all

; 3+(4/2)
FLD FAC, 4.0
FLD AFAC, 2.0
FDIV
FPUSH FAC    ;
FLD FAC, 3.0 ;   better: FLD AFAC, 3.0
FPOP AFAC    ;
FADD
Post Reply