Jump to content
nanochess

IntyBASIC compiler v0.9 recharged! ;)

Recommended Posts

 

I was thinking that if the score didn't update terribly often, you'd just print the new score right where you're adding to the score so it's all in one spot. In that math, if you merely shift cycles from updating the score to displaying the score, it's a wash.

 

But, if you really are calling the "display score" code, say, once per frame, and the score update code is called much less than that (seems like a reasonable assumption), then yeah, you really do want your display update code to be as fast as possible.

 

The way I see it - you will always update the display whenever the score changes. So at worst, they'd be equal. On top of that, there are many situations where I can see a person updating the display even if the score doesn't change. So I'd think that overall, display updates would be >= score updates. *Worst* case it's a wash, but it might help improve things. At least that was my line of thinking.

 

If someone has a use case where you'd update a displayed value but not actually update the display, then that invalidates my thought.

 

Share this post


Link to post
Share on other sites

Well, I'm either more confused or just past the point of trying to follow. All I can tell you is that it works, and it works well. Now this is only with jzintv mind you. It's possible your emulator is a bit more forgiving than a real CPU :)

 

Let me turn this on its head then. My code currently uses nothing but RETURN. Instead, why couldn't I just use END everywhere?

 

 

proc1: PROCEDURE

x=1

if y>0 then END

z=10

END

 

I guess what I'm struggling with is how much of this is semantic, or dogma - and what, if any, practical impact there is. Maybe it's because I was raised on Commodore BASIC, where GOSUB/RETURN *were* the beginning and end declarations for a subroutine, so the concept of "oops, forgot to put an END there" ain't gonna be a concern. The very first thing I do after the word PROCEDURE is RETURN on the very next line. And END, in my mind, ends program execution... yeah I could relearn that but I'm curious to know if I need to.

 

Yeah, I would have failed my university classes for doing this. Just as I'd have failed most of them for using a single GOTO, not initializing every single variable first, not breaking my program into separate files and using includes, writing in BASIC in the first place (BASIC was grade school for me), even contemplating loop unravelling (code clarity is WAY more important than performance in college), using ANY magic numbers ANYWHERE, and a dozen other things I do all the time :P

Share this post


Link to post
Share on other sites

 

The way I see it - you will always update the display whenever the score changes. So at worst, they'd be equal. On top of that, there are many situations where I can see a person updating the display even if the score doesn't change. So I'd think that overall, display updates would be >= score updates. *Worst* case it's a wash, but it might help improve things. At least that was my line of thinking.

 

 

I actually had a different concern in mind. Often times, the point at which I need to add something to the score is often in the middle of a critical path in my engine, for example a collision detection loop. So, that puts some pressure on the cost of adding a new value of the score.

 

In that case, it's not total cycles consumed, but rather total cycles in the critical path that mattered.

Share this post


Link to post
Share on other sites

Well, I'm either more confused or just past the point of trying to follow. All I can tell you is that it works, and it works well. Now this is only with jzintv mind you. It's possible your emulator is a bit more forgiving than a real CPU :)

 

Let me turn this on its head then. My code currently uses nothing but RETURN. Instead, why couldn't I just use END everywhere?

 

The problem is that END is a scoping operator. It closes the procedure. It also causes IntyBASIC to issue an ENDP directive, which closes a lexical level in the assembly file.

 

This isn't about the emulator. It's about the correctness of the generated assembly code (the .ASM file). If you don't use exactly one END per PROCEDURE, the generated assembly code is technically incorrect.

 

That it happens to assemble could be regarded as a bug in the current WIP version of AS1600.

  • Like 1

Share this post


Link to post
Share on other sites

Following up on my previous post: It appears I decided to make nested PROCs available by SDK-1600 Beta4. That feature will remain. I'm not sure if I'll add an error message about missing ENDPs at the end of input. If I do, it'll break the current IntyBASIC compiler.

 

 

I guess what I'm struggling with is how much of this is semantic, or dogma - and what, if any, practical impact there is. Maybe it's because I was raised on Commodore BASIC, where GOSUB/RETURN *were* the beginning and end declarations for a subroutine, so the concept of "oops, forgot to put an END there" ain't gonna be a concern. The very first thing I do after the word PROCEDURE is RETURN on the very next line. And END, in my mind, ends program execution... yeah I could relearn that but I'm curious to know if I need to.

 

What I'd propose for IntyBASIC that imposes a minimum amount of dogma, but would make it "correct":

  • Don't require END for PROCEDURE, make it optional. Let IntyBASIC infer it if it sees another PROCEDURE or the end of input. That way, PROCs inside the generated ASM don't nest unexpectedly.
  • If END immediately follows RETURN, try to optimize out the extra RETURN statement. (As it stands, it's pretty harmless and only adds one decle to the ROM size, and no performance penalty.)

That way, a coding style like yours that uses PROCEDURE/RETURN pairs and no END would continue to work just fine, and for folks that do want to use END, their code works too. In either case, IntyBASIC would generate correct assembly. (Currently, IntyBASIC generates incorrect assembly if you leave off END, but AS1600 is letting it get away with it for the most part for now. That should be fixed.)

Edited by intvnut
  • Like 1

Share this post


Link to post
Share on other sites

 

I actually had a different concern in mind. Often times, the point at which I need to add something to the score is often in the middle of a critical path in my engine, for example a collision detection loop. So, that puts some pressure on the cost of adding a new value of the score.

 

In that case, it's not total cycles consumed, but rather total cycles in the critical path that mattered.

 

Very good point. I've run into something like this, and while I'm nowhere close to cycle counting yet - I've definitely seen cases where I had the thought of "what if this gets a lot more critical down the road?".

 

I think that regardless of which is more important, your routines would be excellent additions to IntyBASIC. :) I can tell just from the comments here that several of us have already re-invented the wheel on this. With resulting assumptions and limitations. For instance, no one seems to have used a declining score. I'm thinking of a large timer as an example, but to be honest much of my thought came from looking at Astrosmash. Score goes up, score goes down, certain events are triggered via score. It's not something common on the INTV and the math routines are one of those things that once you've nailed it, work in all sorts of situations.

 

Share this post


Link to post
Share on other sites

That way, a coding style like yours that uses PROCEDURE/RETURN pairs and no END would continue to work just fine, and for folks that do want to use END, their code works too. In either case, IntyBASIC would generate correct assembly. (Currently, IntyBASIC generates incorrect assembly if you leave off END, but AS1600 is letting it get away with it for the most part for now. That should be fixed.)

 

Oh hey, I hope I'm not coming off as argumentative. I'm just trying to make sure I understand the reasoning behind things. I find it's extremely easy to fall into the trap of copy/paste sample code, muck with it, compile/rinse/repeat without actually understanding what's going on. I do not want to be one of "those" coders - if I'm gonna do this, I want to do it right. I'm not trying to advocate for any particular style, I was genuinely curious why something that officially shouldn't work, does. Bugs or leniency on the part of a compiler/assembler/emulator are not the kinds of things I want to exploit, they always bite you in the ass eventually.

 

So let me ask you another one: nested procs work in the emulator, and I'd assume on the real CPU. Why wouldn't you have allowed it in the assembler? Just being cautious in case you didn't get the implementation exactly right? When I was looking at where I've used them it was almost by accident (and I know this could easily lead to a lecture on "this is why programs turn into spaghetti and/or break and/or are inscrutable 3 months later") but I'll be honest, it's damned handy.

 

What I'm finding is that IntyBASIC feels much more like C - or maybe Pascal - than BASIC. It's a lot easier to write structured code with tons of re-use opportunities. I'm constantly re-factoring my code, partly because I'm OCD but also because it makes it so much more understandable the next day. I don't recall BASIC being quite so flexible when I learned it. As I've mentioned several times, between a pretty slick compiler, and the generally-overlooked-but-absolutely-critical feature-rich assembler, I am almost speechless at how quickly I can bang out a workable prototype game. Or mock up an idea, or test an example, or whatever. I've never worked in an environment that is so simple, powerful, and flexible, all at the same time. I'm used to either being restricted to baby-simple implementations, or dealing with a ridiculous amount of setup before you can write a character to the screen.

 

Anyway, enough fawning. Suffice it to say that this community continues to impress me. I only hope I can give something fun back in return.

  • Like 1

Share this post


Link to post
Share on other sites

When you say "nested procs," do you mean nesting the procedures declaratively, such as:

myprocedureA: PROCEDURE

myprocedureB: PROCEDURE

END

END 

Or do you mean nested GOSUBS, such as:

GOSUB myprocedureA

myprocedureA: PROCEDURE
GOSUB myprocedureB
END

myprocedureB: PROCEDURE
GOSUB myprocedureC
END

myprocedureC: PROCEDURE

END

?

 

Because if the former, I don't know why you'd ever want to do that. If the latter, then you certainly CAN, but there are a limited number of nests you can call. If I understand correctly, each time a GOSUB is called, the current instruction address is pushed to the stack and the procedure's instruction address is called instead. When a RETURN is called, the previous address is popped from the stack and the code continues from where it left off. However, the stack is finite and can only hold so many instruction addresses, so you have to make sure you RETURN from your procedures or the stack will clog with addresses that never get popped back off.

Share this post


Link to post
Share on other sites

 

Oh hey, I hope I'm not coming off as argumentative. I'm just trying to make sure I understand the reasoning behind things. I find it's extremely easy to fall into the trap of copy/paste sample code, muck with it, compile/rinse/repeat without actually understanding what's going on. I do not want to be one of "those" coders - if I'm gonna do this, I want to do it right. I'm not trying to advocate for any particular style, I was genuinely curious why something that officially shouldn't work, does. Bugs or leniency on the part of a compiler/assembler/emulator are not the kinds of things I want to exploit, they always bite you in the ass eventually.

 

Oh no, not at all. The thing about BASIC is that it was one of the original RAD languages, really. (RAD == Rapid Application Development.) It offers flexibility to allow you to get started quickly. If you want your project to scale, you have to bring the discipline. But, you're not required to bring the discipline up front (which lets you get a prototype ready pronto), and you can decide for yourself what discipline matters and what doesn't.

 

 

 

 

 

So let me ask you another one: nested procs work in the emulator, and I'd assume on the real CPU. Why wouldn't you have allowed it in the assembler? Just being cautious in case you didn't get the implementation exactly right? When I was looking at where I've used them it was almost by accident (and I know this could easily lead to a lecture on "this is why programs turn into spaghetti and/or break and/or are inscrutable 3 months later") but I'll be honest, it's damned handy.

 

 

The emulator doesn't give two shakes what you write. It doesn't see the original code, or have any concept of procedures or nesting or anything else. It just sees instructions.

 

The assembler has PROC/ENDP constructs that define scoping for labels. None of this is visible to the emulator or the Intellivision. It's only visible to the assembler, and is used mainly to construct local label names.

 

Nested PROC/ENDP pairs do work in the Beta4 and later AS1600. But, they're quirky as sin, which is why I haven't really publicized them. PROC/ENDP is just as scoping concept for labels. If you're willing to dive into assembly for a second, consider the following example:

FOO     PROC

@@1:    DECLE   1       ; defines the symbol FOO.1, points to the value 1 in ROM

BAR     PROC
@@2:    DECLE   2       ; defines symbol FOO.BAR.2, points to the value 2 in ROM
        ENDP    ; end FOO.BAR

BAZ     PROC
@@3:    DECLE   3       ; defines the symbol FOO.BAZ.3, points to the value 3 in ROM
        ENDP    ; end FOO.BAZ
        ENDP    ; end FOO

The weird, quirky thing that happens in AS1600 is that FOO, BAR, and BAZ get declared as top-level symbols. But the @@1, @@2, @@3 get declared as FOO.1, FOO.BAR.2, FOO.BAZ.3. That dichotomy is just... weird.

 

But like I said, PROC/ENDP in the assembler just manage the names of symbols. They don't add instructions to the output or anything else. They're there to allow the programmer to structure their assembly code so that labels don't conflict with each other. The assembler doesn't do anything else. It's up to whatever or whoever generates the assembler to create syntactically valid assembler.

 

Once the assembler produces a .BIN or .ROM, the emulator and the Intellivision don't care.

 

What's happening with IntyBASIC is that when you write something like this:

        GOSUB fred
        GOSUB barney

fred    PROCEDURE
        PRINT AT 0, "FRED"
        RETURN

barney  PROCEDURE
        PRINT AT 0, "BARNEY"
        RETURN

...the IntyBASIC 0.9 compiler outputs something like this:

...prolog code here

    ;
    ;         GOSUB   fred
    CALL Q1
    ;         GOSUB   barney
    CALL Q2
    ;
    ;
    ; fred:   PROCEDURE
    ; FRED
Q1: PROC
    BEGIN
    ;         PRINT AT 0, "FRED"
    MVII #512,R0
    MVO R0,_screen
    MVI _screen,R4
    MVII #304,R0
    XOR _color,R0
    [email protected] R0,R4
    MVII #400,R0
    XOR _color,R0
    [email protected] R0,R4
    MVII #296,R0
    XOR _color,R0
    [email protected] R0,R4
    MVII #288,R0
    XOR _color,R0
    [email protected] R0,R4
    MVO R4,_screen
    ;         RETURN
    RETURN
    ;
    ;
    ; barney: PROCEDURE
    ; BARNEY
Q2: PROC
    BEGIN
    ;         PRINT AT 0, "BARNEY"
    MVII #512,R0
    MVO R0,_screen
    MVI _screen,R4
    MVII #272,R0
    XOR _color,R0
    [email protected] R0,R4
    MVII #264,R0
    XOR _color,R0
    [email protected] R0,R4
    MVII #400,R0
    XOR _color,R0
    [email protected] R0,R4
    MVII #368,R0
    XOR _color,R0
    [email protected] R0,R4
    MVII #296,R0
    XOR _color,R0
    [email protected] R0,R4
    MVII #456,R0
    XOR _color,R0
    [email protected] R0,R4
    MVO R4,_screen
    ;         RETURN
    RETURN
    ;
        ;
        ; Epilogue for IntyBASIC programs
.... more epilog code here

"fred" ended up being Q1, and "barney" ended up being Q2. What ends up happening is that IntyBASIC's output opens a PROC for both "fred" and "barney", but it doesn't close either with an ENDP. So, both Q1 and Q2 get a top level label, but all the epilog code gets jammed into a triply nested PROC.

 

The first function in the epilog code is CPYBLK:

        ;
        ; Copy screen helper for SCREEN statement
        ;
CPYBLK: PROC
        PSHR R5
        MOVR R3,R4
        MOVR R2,R5

@@1:    MOVR R1,R3              ; Init line copy
@@2:    [email protected] R4,R2              ; Copy line
        [email protected] R2,R5
        DECR R3
        BNE @@2
        MVII #20,R3             ; Add offset to start in next line
        SUBR R1,R3
        ADDR R3,R4
        ADDR R3,R5
        DECR R0                 ; Count lines
        BNE @@1

        PULR R5
        JR R5
        ENDP

Under normal circumstances, this would generate three assembler labels: CPYBLK, CPYBLK.1 (which corresponds to @@1 above) and CPYBLK.2 (which corresponds to @@2 above). But, with this bug/misfeature, AS1600 actually assigns @@1 and @@2 the names Q1.Q2.CPYBLK.1 and Q1.Q2.CPYBLK.2. The resulting code still assembles. But... I can't help but feel this isn't really what was intended. It works in spite of itself.

 

 

 

 

 

 

What I'm finding is that IntyBASIC feels much more like C - or maybe Pascal - than BASIC. It's a lot easier to write structured code with tons of re-use opportunities. I'm constantly re-factoring my code, partly because I'm OCD but also because it makes it so much more understandable the next day. I don't recall BASIC being quite so flexible when I learned it. As I've mentioned several times, between a pretty slick compiler, and the generally-overlooked-but-absolutely-critical feature-rich assembler, I am almost speechless at how quickly I can bang out a workable prototype game. Or mock up an idea, or test an example, or whatever. I've never worked in an environment that is so simple, powerful, and flexible, all at the same time. I'm used to either being restricted to baby-simple implementations, or dealing with a ridiculous amount of setup before you can write a character to the screen.

 

If you look to the later BASICs of the 80s, many had good features for structuring programs. TI Extended BASIC (which I spent quite a lot of time with in the 80s) had named subroutines with parameters, as did QuickBASIC/QBASIC and the TrueBASIC I mentioned up-thread.

 

And I'm certain they were borrowing from the Pascal heritage, as the original Dartmouth BASIC had none of these things, nor did most Microsoft BASICs before QuickBASIC/QBASIC.

  • Like 1

Share this post


Link to post
Share on other sites

The latter. It never occurred to me that someone would write the former, except perhaps in a drunken coding session. I don't even know how you'd determine precedence (what does END .. end?).

 

I just think in terms of pushing and popping the stack. Your understanding of how it works is correct, at least on any platform I've ever worked on. And yup, already been caught being dumb about it :P You do raise a good question in my head though - how deep IS the PC stack here? And - I'm embarrassed to say that I just don't remember these things anymore - is this determined by the compiler, assembler, or hardware? Who makes the decision of how many addresses you can place on the stack before overflowing (ie: how is the size set)? Thumb in the air tells me I can go either 8 or 16 deep based on when I started seeing problems last time (didn't count closely, to be honest). Maybe this is something that should be documented along with the stack protection code nanochess put in IntyBASIC.

 

Oh, and just to be a real ass about things - a user-declarable stack size would be hella fun. I'm a big fan of recursion, when done right. It'd be a hoot to design some messed up graphics routines that recurse 20, 50, 100 levels deep. Just for demo purposes of course.

Share this post


Link to post
Share on other sites

When you say "nested procs," do you mean nesting the procedures declaratively, such as:

myprocedureA: PROCEDURE

myprocedureB: PROCEDURE

END

END 

 

 

The current assembler allows you to nest PROCs that way, and it's there mainly as a way to scope names, and that only. It just controls how local labels get defined.

 

 

 

In Pascal, you can nest Procedures and Functions, and the main purpose there is also to control scope. It actually does this properly.

 

For example, suppose you're writing an algorithm that at its heart is recursive. You don't want to expose the guts, but you want to provide a nice interface. Being almost 20 years removed from writing any Pascal, hopefully you'll forgive me for writing QuickSort as Pascal pseudo-code.

Procedure QuickSort( ... )
    Procedure PartitionAndSwap( ... )
    Begin
        .. pick a pivot
        .. partition
        .. recurse on left/right partitions if they're non-empty and non-trivial
    End

    Begin
        PartitionAndSwap( array, 0, length - 1 )
    End
End

In Pascal, nested Procedures can also see the local variables of their enclosing functions. AS1600 doesn't really have any notion of variables, just labels, so that also breaks down a bit in AS1600's nested PROC support.

 

 

For IntyBASIC, I wasn't proposing adding support for nested PROCEDUREs. All I was saying was that forgetting "END" at the end of a PROCEDURE in IntyBASIC resulted in assembly code output that was unintentionally relying on AS1600's support for nested PROC/ENDP pairs. It was leaving off the ENDPs in the generated assembler and so technically was generating incorrect output that currently just happens to work.

Edited by intvnut

Share this post


Link to post
Share on other sites

Wish I had taken compiler construction 201. I'm following you, but not in the sense that I'm about to tear apart my own asm to detect future problems. So perhaps it's best that I teach my brain that END functions as RETURN here and be done with it. :) Your comment about epilogue code makes me wonder if I'm not asking for some unpleasant side effects once I go a bit crazy on the tricks.

 

Incidentally, what I meant by "the emulator allowing nested subroutines" was in terms of does the hardware support it, period. Obviously the INTV does. I seem to recall architectures that had a PC - and that was it. Or a one-level deep stack, handled by register. Or no stack pointer, or what have you. So you could hop around in code, but only to a very limited extent. You certainly couldn't nest without some complex tricks. Who knows, I may be remembering theoretical examples of bad architectures.

Share this post


Link to post
Share on other sites

The latter. It never occurred to me that someone would write the former, except perhaps in a drunken coding session. I don't even know how you'd determine precedence (what does END .. end?).

 

Oh, that's one's easy. It always ends the innermost level. It's the same as this nested FOR construct that was perfectly valid in all the Microsoft BASICs:

FOR I = 1 to 20
    FOR J = 1 to 20
        FOR K = 1 to 20
            ....
        NEXT
    NEXT
NEXT
I just think in terms of pushing and popping the stack. Your understanding of how it works is correct, at least on any platform I've ever worked on. And yup, already been caught being dumb about it :P You do raise a good question in my head though - how deep IS the PC stack here? And - I'm embarrassed to say that I just don't remember these things anymore - is this determined by the compiler, assembler, or hardware? Who makes the decision of how many addresses you can place on the stack before overflowing (ie: how is the size set)? Thumb in the air tells me I can go either 8 or 16 deep based on when I started seeing problems last time (didn't count closely, to be honest). Maybe this is something that should be documented along with the stack protection code nanochess put in IntyBASIC.

 

I don't know how it's set for IntyBASIC. The hardware doesn't place much constraint on it other than the available memory, and a minimum size based on the interrupt dispatch code.

 

Interrupts on the Intellivision take 8 words minimum. That's baked into the EXEC, and cartridges can't easily override it. I tend to size my stacks at 32 words when I start, and then trim them back as needed. (I think I aimed for a 16 word stack in Space Patrol, but off hand I'm not certain.)

 

 

Oh, and just to be a real ass about things - a user-declarable stack size would be hella fun. I'm a big fan of recursion, when done right. It'd be a hoot to design some messed up graphics routines that recurse 20, 50, 100 levels deep. Just for demo purposes of course.

 

You may be better off with data-oriented recursion. The 'flood fill' routine in Same Game & Robots (used for scoring) is recursive, and to make it work, I encoded the "stack" into some spare bits in each BACKTAB word. That way, the algorithmic recursion depth could be as deep as "the entire screen", and it was guaranteed to work. Likewise for the Maze Demo that comes with SDK-1600.

 

The unexpanded Intellivision only has 352 words of 16-bit memory. Stack must live in 16-bit memory. The display takes 240 of those 16-bit words, leaving only 112 to IntyBASIC and you.

Edited by intvnut

Share this post


Link to post
Share on other sites

Wish I had taken compiler construction 201. I'm following you, but not in the sense that I'm about to tear apart my own asm to detect future problems. So perhaps it's best that I teach my brain that END functions as RETURN here and be done with it. :) Your comment about epilogue code makes me wonder if I'm not asking for some unpleasant side effects once I go a bit crazy on the tricks.

 

Incidentally, what I meant by "the emulator allowing nested subroutines" was in terms of does the hardware support it, period. Obviously the INTV does. I seem to recall architectures that had a PC - and that was it. Or a one-level deep stack, handled by register. Or no stack pointer, or what have you. So you could hop around in code, but only to a very limited extent. You certainly couldn't nest without some complex tricks. Who knows, I may be remembering theoretical examples of bad architectures.

 

You might spend some time looking at the assembly that IntyBASIC outputs. It's really quite simple and not hard to follow.

 

It interlists your original BASIC code with the code it generates. I think after a little while you'll get a better feel for what your code turns into and what actually runs on the real machine.

 

Having the two side by side is a pretty gentle on-ramp to understanding assembly.

 

The general structure of IntyBASIC output is straightforward:

  • IntyBASIC prolog code. This handles ROM headers and certain setup
  • Your code, converted in a fairly straightforward line-by-line manner into CP-1600 assembly. The original BASIC shows up as comments, and the resulting assembly code is right after.
  • IntyBASIC epilog code. This provides some library functions and so on that implement the rest of the IntyBASIC runtime.

The prolog/epilog come from a couple of ASM files provided with IntyBASIC as I recall.

 

 

As for the IntyBASIC "END" keyword, it really should only be used as the last statement of a PROCEDURE, not as a general replacement for RETURN. (ie. if you have an early exit from a PROCEDURE, it needs to use RETURN, not END.)

 

And the shortcomings in the IntyBASIC compiler described upthread that cause it to output technically-incorrect assembly code should really just be fixed by nanochess. I think they're fairly minor and should be straightforward to fix. IntyBASIC programmers shouldn't have to worry about those details so much.

  • Like 1

Share this post


Link to post
Share on other sites

Oh hey, I hope I'm not coming off as argumentative. I'm just trying to make sure I understand the reasoning behind things. I find it's extremely easy to fall into the trap of copy/paste sample code, muck with it, compile/rinse/repeat without actually understanding what's going on. I do not want to be one of "those" coders - if I'm gonna do this, I want to do it right. I'm not trying to advocate for any particular style, I was genuinely curious why something that officially shouldn't work, does. Bugs or leniency on the part of a compiler/assembler/emulator are not the kinds of things I want to exploit, they always bite you in the ass eventually.

Oh, not at all! That's why we're in this forum: to ask, learn, share, and yes, debate the finer points of making retro-games.

 

Like I say to the guys at work, "there are no stupid questions, only stupid people asking them." (Just kidding!!) :lol:

 

Anyway, let me try my hand at giving you another explanation of what's going on.

 

Like many high-level languages, BASIC supports structured programming. Structure programming is a way of encapsulating chunks of functionality into manageable and fully-contained blocks. An advantage of this is that it allows for re-use of these functional blocks. Also, it allows you to reason about your game logic at a higher level, by offering a concise representation of the flow that "hides" away the details.

 

For example:

IF (KEY = UP) THEN
    GOSUB MOVE_UP
ELSE IF (KEY = DOWN) THEN
    GOSUB MOVE_DOWN
ELSE
    GOSUB FLOAT_AWAY
END

Looking at that code, you can tell at a glance what it does. It won't tell you how it does it, that's a lower level detail that you can figure out later, but in the grand scheme of things it is not essential to your flow. Thus, you can separate the "what" from the "how" into smaller manageable chunks, to which you can apply effort as needed.

 

For instance, if the "flow" of your game needs to change (say, now you have to support moving side-ways), this has no effect in the functionality that already moves vertically. It becomes obvious where to put the changes, and implementing them can be done at your pace, one block at a time.

IF (KEY = UP) THEN
    GOSUB MOVE_UP
ELSE IF (KEY = DOWN) THEN
    GOSUB MOVE_DOWN
ELSE IF (KEY = LEFT) THEN
    GOSUB MOVE_LEFT
ELSE IF (KEY = RIGHT) THEN
    REM Still need to do this...
    REM GOSUB MOVE_RIGHT
ELSE
    GOSUB FLOAT_AWAY
END

Now, I want to direct you to the above code to take note of an innocuous and simple directive: END. It seem almost like an afterthought, but it is important: it tells the compiler where a particular block of code ends. Take for instance the following block:

IF (HIT = 1) THEN
    GOSUB KILL_ENEMY
    GOSUB DO_EXPLOSION
    GOSUB UPDATE_SCORE
    GOSUB UPDATE_PLAYER
END

That's a nice and concise block of code that tells you exactly what is going on, right? If that END weren't there, how would the compiler know to distinguish between the conditional block and the stuff around it, so that it knows to skip it if necessary?

 

That right there is the essence of structure: A block of code that encapsulates a particular function, with clear start and end boundaries, and precise entry and exit points.

 

However, as I've alluded above, a program is not mere structure, it requires flow control. Flow control is what binds the structure together and allows your program to take action. If structure is the body of your program, flow control is its mind: it tells it what to do and when to do it.

 

Structure and flow control work together, but they are separate beasts--remember that.

 

 

Now, on to the GOSUB/PROCEDURE/RETURN statements which started this whole discussion. In BASIC, GOSUB and PROCEDURE (typically known as SUB for sub-routine in other dialects of BASIC) are the poster-children of structure programming, both structure and flow control wrapped into one neat package. The structure part is in the definition of the sub-routine block with PROCEDURE. The flow control is in the logical branching directed by GOSUB.

 

Now, in defining a sub-routine, think back to our IF/END example: you need to tell the compiler where the block starts and ends:

MY_SUB: PROCEDURE
    REM Do your stuff,
    REM oh grand and mighty
    REM sub-routine!
END

In controlling the flow, the GOSUB statement tells the compiler, "at this point, branch out to this sub-routine; once you're done with it, return right back here."

 

That's it, really.

 

What about RETURN? Don't I need to return? Well, remember, this is new and fancy-pants structure programming BASIC, none of that old and creaky stuff with line numbers. Back in the day, you told the compiler "from here, GOSUB to there," and from there you told the compiler "I'm done, RETURN to here." There were no blocks, there was no real structure (at least not in the way we understand it now). These heres and theres where imbedded within your regular code, making it sort of confusing, and increasing your cognitive burden as you read through your program.

 

Structured programming replaced this with actual separated blocks of code. Now you can put them in modules, or surround them with empty lines to make their segregation obvious and understandable. The start and end points of a sub-routine defined its scope, and so you didn't need to explicitly RETURN any more.

 

However, the RETURN statement remained because it is exceedingly useful. It allows for even finer flow control within your sub-routine, by allowing it to return early if conditions direct it so.

MY_SUB: PROCEDURE
    REM Do stuff here...

    IF (DONE = 1) THEN
        REM I'm done get me outta here!
        RETURN
    END

    REM Not done, proceed young padawan.
    REM Do more stuff here...

    REM I could RETURN here...
    REM but I don't hafta. Neener neener! 
END

As you can see, the last RETURN statement is optional, since the compiler will find the end of the procedure and return to the caller anyway, which is it's default behaviour. And that's the right way to think of it: structure vs. flow control. A sub-routine has a beginning and an end, and by default it will return back to its caller when it's done. You can, however, fine-tune its flow by finishing early in certain conditions, and returning before the end of the block.

 

So it goes like this:

 

GOSUB, execute procedure, then END and return to caller.

OR...

GOSUB, execute procedure, conditionally RETURN early, or END and return to caller.

And there you go. I hope this helps. :)

 

-dZ.

Edited by DZ-Jay
  • Like 1

Share this post


Link to post
Share on other sites

DZ-Jay - Thanks :)

 

After sleeping on it, I'm realizing that most of my consternation here comes from the 2 sides of my programming background:

 

1. Commodore BASIC, where END did not exist (not in this context anyway). RETURN was all you had. Things were chaotic and that's how we liked it.

 

2. Later on, being formally trained in C by someone who was absolutely convinced that not only was GOTO evil in virtually all situations, but that there is never, EVER any excuse to "prematurely break program flow" within a function. Every single piece of code should naturally flow to the end of the function, and return from there. And to be honest, he was right - in the sense that you can always code this way without a lot of effort or performance hit. His methodology revolved around eliminating any spaghetti and any hopping around within code. It was one of the most structured, disciplined approaches to application development I've ever seen. And the man worked in realtime embedded systems (his code was in half of the computer-controlled automobile transmissions on the road by the late 90s) so it's not like he didn't know how to code for performance - this was something he had to convince a lot of doubters about.

 

So essentially, the concept of a "middle ground" here is what you guys are advocating (in fact it's what the tools are designed for). And my peabrain is just gonna have to accept things. :P

  • Like 1

Share this post


Link to post
Share on other sites

So essentially, the concept of a "middle ground" here is what you guys are advocating (in fact it's what the tools are designed for). And my peabrain is just gonna have to accept things. :P

Not quite, at least it's not what I'm advocating. I just explained the semantic and functional difference between program structure and flow. That BASIC allows you to construct "spaghetti code" is neither here nor there.

 

For a truly structured approach, in your professor's terms, you can definitely avoid premature exits on any sub-routine; just avoid the RETURN statement completely. It is true that this forces you to think functionally and may lead to a more elegant program flow.

 

However, it also has the potential of forcing you to write convoluted or stilted code, when a more intuitive construction would have been more concise.

 

In my view, your code should express your program's structure and flow in as close a way as how you reason it.

 

If your mental model says, "don't process if condition fails," then it may be easier to recognize when expressed as such:

MY_PROC: PROCEDURE
    IF (TEST = FALSE) THEN
        RETURN
    END

    REM Do the deed...

END
If instead you reason it as "only proceed if condition succeeds," then it may seem more natural to read:

MY_PROC: PROCEDURE
    IF (TEST = TRUE) THEN

        REM Do the deed...

    END
END
Both of them are functionally equivalent and it is absolutely debatable which one is superior, because it depends on the programmer's frame of mind. As with many other things in life, there is no One True Way. The key is to pick a model and stick to it.

 

Inconsistency is the path to the Dark Side. Inconsistency leads to anger. Anger leads to hate. Hate leads to suffering. ;)

 

Ultimately, GOSUB and RETURN--and even GOTO--are just additional tools in your kit. Like any tool, they can be used to great success in solving a problem for which they are suited, or they can be used to bash someone's head in. The choice is yours. :)

 

dZ.

Share this post


Link to post
Share on other sites

DZ-Jay - Thanks :)

 

After sleeping on it, I'm realizing that most of my consternation here comes from the 2 sides of my programming background:

 

1. Commodore BASIC, where END did not exist (not in this context anyway). RETURN was all you had. Things were chaotic and that's how we liked it.

 

2. Later on, being formally trained in C by someone who was absolutely convinced that not only was GOTO evil in virtually all situations, but that there is never, EVER any excuse to "prematurely break program flow" within a function. Every single piece of code should naturally flow to the end of the function, and return from there. And to be honest, he was right - in the sense that you can always code this way without a lot of effort or performance hit. His methodology revolved around eliminating any spaghetti and any hopping around within code. It was one of the most structured, disciplined approaches to application development I've ever seen. And the man worked in realtime embedded systems (his code was in half of the computer-controlled automobile transmissions on the road by the late 90s) so it's not like he didn't know how to code for performance - this was something he had to convince a lot of doubters about.

 

So essentially, the concept of a "middle ground" here is what you guys are advocating (in fact it's what the tools are designed for). And my peabrain is just gonna have to accept things. :P

Yeah, I don't like writing "goto's" either. But given that currently in Intybasic, an "if" statement has to have all it's logic in one line, I find it cleaner to use goto's for anything non trivial.

 

Instead of writing :

 

If A=0 then blah1 : blah2 : blah3 : blah4 : blah4 :blah5

 

which leads to really long lines of code, I would rather write:

 

If A <> 0 then goto skip_25

rem a= 0 case

blah1

blah2

blah3

blah4

blah5

skip_25:

 

Which looks like spaghetti code with the goto, but is more readable for me than really wide horizontal lines of code...

Share this post


Link to post
Share on other sites

It's kind of easy to check for missing END at end of PROCEDURE :), it will be done for next version of IntyBASIC.

Share this post


Link to post
Share on other sites

BTW, I didn't want to go ahead with the block notion of C language (QuickBASIC style) in IntyBASIC since start, because I've noted that most people understands easily the line-by-line concept, but the blocks concept is a completely different beast!

Share this post


Link to post
Share on other sites

By block, I assume you're referring to the way that functions in C languages are like boxes stacked within boxes? There's a bit of that with procedures in BASIC, right?

Share this post


Link to post
Share on other sites

By block, I assume you're referring to the way that functions in C languages are like boxes stacked within boxes? There's a bit of that with procedures in BASIC, right?

 

True, but only one level of block structuring. When IntyBASIC programs start getting bigger, they could serve as natural 'cut points' for splitting the ROM into different segments under the hood, to make it easy to take advantage of disjoint memory maps (such as the Mattel 5/6/D/F memory map).

 

If IntyBASIC can determine the size of each PROCEDURE before it prints it out, then it can issue a ROMSEGSZ declaration ahead of the PROC and cart.mac will whisk it away to whatever ROM segment has sufficient room. It shouldn't be hard to add those declarations to the prolog/epilog code too.

 

As it currently stands, once anyone gets a game over 8K words (16K bytes), I imagine the game will crash immediately on startup, as the memory image will spill over into $7xxx. The Intellivision EXEC will jump there instead of the usual entry point via the ROM header, and wackiness will ensue.

Share this post


Link to post
Share on other sites

As it currently stands, once anyone gets a game over 8K words (16K bytes), I imagine the game will crash immediately on startup, as the memory image will spill over into $7xxx. The Intellivision EXEC will jump there instead of the usual entry point via the ROM header, and wackiness will ensue.

 

Wait wait wait. There was a fair bit of discussion in the GoSub thread about how big his game could be. As he approached 16BK, people kept reassuring him that a more than 16KB game is just fine for homebrew, that you could go up to 32KB. Were they only addressing (heh) the modern PCB ROM size?

 

I've *always* tried to keep under 16KB, and then read some of these comments and thought "shit, I can double the size!". Are you saying IntyBASIC will limit me to 16KB as it stands? That's not a huge problem at the moment, I just want to be abundantly clear on this. And possibly to start funding (ie: bribing) some IntyBASIC development if I find myself needing more space. :)

Share this post


Link to post
Share on other sites

 

Wait wait wait. There was a fair bit of discussion in the GoSub thread about how big his game could be. As he approached 16BK, people kept reassuring him that a more than 16KB game is just fine for homebrew, that you could go up to 32KB. Were they only addressing (heh) the modern PCB ROM size?

 

I've *always* tried to keep under 16KB, and then read some of these comments and thought "shit, I can double the size!". Are you saying IntyBASIC will limit me to 16KB as it stands? That's not a huge problem at the moment, I just want to be abundantly clear on this. And possibly to start funding (ie: bribing) some IntyBASIC development if I find myself needing more space. :)

 

 

I honestly don't know how IntyBASIC manages the ROM memory map or even if it makes any attempt to manage the ROM memory map, aside from just putting ORG $5000 at the top of the output. In the simple examples I've run through it, it appears to start assembling the game at $5000 and goes linearly after that.

 

If that's the case, then yes, you'll run into a hiccup once the ROM gets big enough to cross the $7000 boundary. That's 8K words (16K bytes).

 

IntyBASIC does have an inline ASM "escape valve," and you could use that to insert ORG directives to move things around. That's clumsy and error prone and far from user friendly.

 

You could modify intybasic_prologue.asm to pull in "cart.mac", and use ASM statements to insert "ROMSEG" or "ROMSEGSZ" types of directives (really, macros from cart.mac) to move things around. This is slightly less error prone.

 

Or, IntyBASIC could inject these things itself. (Best option, IMHO.) Either it can manage the memory map directly, or it can offload to an abstraction like cart.mac. I honestly don't care either way.

 

 

The largest game you can write without paging the ROM is around 50K words (100K bytes). But to get there, something needs to allocate the code among several disjoint memory areas. The cart.mac facility I wrote is one way to manage it. I'd be happy to help nanochess teach IntyBASIC how to drive its macros so that it simplifies the end programmer experience. Or, IntyBASIC could handle this itself. (Maybe it does already?)

 

The way I set up cart.mac, you can add as many different memory maps as you like, to cater it to whatever board design or other criteria you need to apply. Right now it has two memory maps in the official version—16K, which matches the Mattel 5/6/D/F layout, and 42K, which seemed like a nice Cadillac layout that works well with JLP. (JLP actually supports fairly arbitrary ROM memory maps, although currently it fixes its extra RAM at $8040 - $9F7F.) But, say you're working with a particular board design that wants 5/6/9/A/B. You could add that to cart.mac pretty easily.

 

 

So, nanochess, does IntyBASIC manage the ROM memory map, and if so, how? And if not, does it make sense to target something like cart.mac? I recall you commenting in the past that programmers are free to modify intybasic_prologue.asm/epilogue.asm to suit their needs, including memory map, but I'm not sure that fits with the ease of use you're promoting with the rest of IntyBASIC. :-)

Share this post


Link to post
Share on other sites

 

Wait wait wait. There was a fair bit of discussion in the GoSub thread about how big his game could be. As he approached 16BK, people kept reassuring him that a more than 16KB game is just fine for homebrew, that you could go up to 32KB. Were they only addressing (heh) the modern PCB ROM size?

 

I've *always* tried to keep under 16KB, and then read some of these comments and thought "shit, I can double the size!". Are you saying IntyBASIC will limit me to 16KB as it stands? That's not a huge problem at the moment, I just want to be abundantly clear on this. And possibly to start funding (ie: bribing) some IntyBASIC development if I find myself needing more space. :)

 

Very easily you can get your game to 32 kilobytes (16K words) using the standard zones $5000-$6FFF, $D000-$DFFF and $F000-$FFFF.

 

Or simply use $5000-$6FFF, $A000-$BFFF and $C100-$FFFF like I did in Princess Quest.

 

It's so easy as checking the CFG file to see the memory zones used, if one is exceeded, you can check the LST file and insert an ASM ORG directive to displace manually your program.

 

I would suggest strongly to keep graphics/music/data in upper zones to simplify management.

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...