Jump to content
  • 0

BASIC - Out of Memory


zerothis
 Share

Question

Warning: I'm new to Commodore/Commander x16 BASIC. And 35 years out of practice in BASIC. And never really that advanced to begin with. And, I'm working on someone's else's code that I cannot make public at this time.

I have a linear array of 6 values. The code uses FOR-NEXT to fill all 6 values with a random integer between 3 and 25. The square root of the RND value is calculated before being being multiplied (in case that makes a difference) . The screen is cleared then the values are printed on screen and the user is prompted (GET B$) to accept them or choose 6 new random values. If the user reject them, then the program goes back to the FOR-NEXT loop to create 6 new values. Problem is, if they reject the values 19 times, the program crashes with "?OUT OF MEMORY  ERROR IN 650 (the line with the FOR-NEXT and RND). I can use CLR before the FOR-NEXT; no OOM error. But this borks all the arrays in the program that are filled using DATA statements. The FRE(0) function seems like it is applicable I cannot understand how to use it to clear out the array (if that is what causes the OOM)?

 

 

 

Link to comment
Share on other sites

16 answers to this question

Recommended Posts

  • 0

It could be strings heap issues generated by your "GET" function, but as its erroring out in the FOR/NEXT loop I'd say you've got some coding issues that are smashing the BASIC stack. 

Whenever BASIC does a GOSUB, or starts a FOR/NEXT loop it adds pointers to the basic stack so it can "Return" from the GOSUB or get back to the start of the FOR/NEXT loop.  When you issue a RETURN, the BASIC interpreter reads the most recent (last in first out) GOSUB pointer,  deletes it from the BASIC stack, and then jumps to the location of the pointer to bring code execution back to the point right after the GOSUB statement.    Likewise, when a FOR/NEXT loop completes its run (i.e., the value of the indexing variable (lets call it "I")  has reached the final end value (call it "Z" for purposes of this discussion), i.e.,  FOR I = 1 to Z" value, then the BASIC interpreter clears out the pointer in the BASIC stack that it was using to jump back to the beginning of the loop when it reached the "NEXT" operator.      

If you "GOSUB" somewhere, and then never execute a corresponding RETURN for that GOSUB, but somehow wind up back at that code line and issue that same GOSUB line again, you'll add a new pointer to the BASIC stack without the old one getting deleted.   If you exit a FOR/NEXT loop OTHER than by the loop completing of its own accord, then the FOR/NEXT pointers won't get removed.     

BASIC tries to help you with this.    If you start a FOR/NEXT loop in a subroutine reached by GOSUB, then all pointers related to that FOR/NEXT loop are cleaned out (whether those loops exited normally or not) when the interpreter executes the RETURN statement from that subroutine. 

But generally, you need to be aware of GOSUB/RETURN and FOR/NEXT finishing "the right way" as you code.  Think of these as "the other shoe must drop" situations, you have to 'close out the cycle" cleanly or the BASIC stack grows until it has no room for the next FOR/NEXT pointer or GOSUB/RETURN pointer.   

Another thing:  Although you can nestle FOR/NEXT loops (I.e., FOR Y=0to25:FORX=0to39: [stuff]: NEXTX:NEXTY) C64 BASIC limits this to 10 maximum, or again it errors out.   And the error in all these cases will be:

"OUT OF MEMORY ERROR"

One thing novice BASIC programmers do is think that they need to use a different variable name for every FOR/NEXT loop.   That's not so, and it actually inadvertently avoids another cleanup tool built into C64 basic.   Suppose I have a loop early in my program that is indexed by "I'  ("FOR I = ...") and that I use to read a bunch of data before branching to my main program.     But suppose that I had a brain fart and coded my DATA reader to exit with a GOTO when it got to  a "-1" I put at the end of my values.    Boom, I've left a FOR/NEXT pointer from a premature FOR/NEXT exit on the BASIC stack.    But suppose I then get to my splash screen routine, and need to loop through 10 messages for the user, and decide to use the "I" variable again to index that FOR/NEXT loop.   BINGO!   Whenever you start a new FOR/NEXT using the indexing variable of another FOR/NEXT loop that has been left on the stack, the interpreter cleans up the mess from the OLD loop before starting the new one. 

The lesson is to look at your code and make sure you're using the same indexing variable over again for subsequent loops (assuming the original loops are done and the indexer variable name from an earlier loop has not been defined for something else).   

There's one more benefit to this for C64  BASIC programmers.   If you throw away another scaler variable every time you start a new FOR/NEXT loop, even if there is never again a use for the variables you used to index prior loops, you begin to slow down your program. 

Every scaler variable (not an array) is put in variable-land the first time you use it.    But this is not like a phonebook.   The computer doesn't know the memory address for any particular variable.   It sticks a sequential list of scaler variables and the addresses of their contents right after BASIC in memory, and it uses the "nope, nope, nope" approach to check each variable to see if that's the one you just told it to use.   So if you make 10 different scaler variables, and ask it to use the last one, the interpreter has to scan through all 9 of the earlier created ones EVERY TIME before it gets to the pointer for the one you now want to use.   

Anyway, I think from your description you'll find that something in your code is smashing the BASIC stack.   Look for early loop exits and GOSUBS that you accidentally are treating like GOTOs. 

EDITED TO ADD:    I could be wrong about this part, but I THINK that the GOSUB/RETURN and FOR/NEXT issues can compound each other.   That is to say, although a RETURN from GOSUB cleans out any FOR/NEXTs started during that subroutine, a REPEAT GOSUB to the same routine that contains a FOR/NEXT loop (without the prior GOSUB getting RETURN'd properly) will cause the subroutine to add new FOR/NEXT pointers since it won't look back in the stack past the GOSUB pointer for that subroutine to see that it needs to clean out a FOR/NEXT.    This could be insidious because even if you use the same looping index variable, the stack will have multiple pointers for it.   Lesson:   Start with your GOSUBs as your very first debugging step and make absolutely sure you don't have any that aren't getting "RETURN" statements. 

Edited by Snickers11001001
  • Like 1
Link to comment
Share on other sites

  • 0

I believe arrays are only allocated when they are dimensioned, so the array is likely not the source of the problem. Is it possible that there is a FOR loop that never exits? BASIC will produce an out of memory error if more than nine FOR loops are nested. If this is not the case, try removing all strings from the code segment, as these are reallocated often.

  • Thanks 1
Link to comment
Share on other sites

  • 0

I guess it depends on how big the program is to begin with, you mentioned you're working with someone else's code, so I assume this routine is part of a larger program.

If you're doing any kind of string concatenation inside the loop, that could be filling up memory.  BASIC normally uses strings "in place", but "dynamic" strings fill BASIC memory from the top down, and if the program is very large to begin with (mid to high 30's kb), it's feasible you could be filling up available memory.

To rule out the routine itself, you could code just what you're doing in the routine as a separate program and see if you have the same problem.

 

Edited by x16tial
  • Like 1
Link to comment
Share on other sites

  • 0
4 hours ago, zerothis said:

all the arrays in the program that are filled using DATA statements

Just caught this as well.  If this is a large amount of data, this could help cause the memory problem too.  BASIC is basically storing everything twice, once in the data statements, and then again in the arrays.  (If you want efficiency, BASIC isn't the language 🙂 )

Edited by x16tial
Link to comment
Share on other sites

  • 0
7 hours ago, zerothis said:

Warning: I'm new to Commodore/Commander x16 BASIC. And 35 years out of practice in BASIC. And never really that advanced to begin with. And, I'm working on someone's else's code that I cannot make public at this time.

I have a linear array of 6 values. The code uses FOR-NEXT to fill all 6 values with a random integer between 3 and 25. The square root of the RND value is calculated before being being multiplied (in case that makes a difference) . The screen is cleared then the values are printed on screen and the user is prompted (GET B$) to accept them or choose 6 new random values. If the user reject them, then the program goes back to the FOR-NEXT loop to create 6 new values. Problem is, if they reject the values 19 times, the program crashes with "?OUT OF MEMORY  ERROR IN 650 (the line with the FOR-NEXT and RND). I can use CLR before the FOR-NEXT; no OOM error. But this borks all the arrays in the program that are filled using DATA statements. The FRE(0) function seems like it is applicable I cannot understand how to use it to clear out the array (if that is what causes the OOM)?

Ah so it's doing character generation with random rolls for each of six characteristics (Str Dex End Int Wis Cha or something).

Going on your description above, I'll guesstimate the code fragment:

100 dim a[5] 
650 for x = 0 to 5: a[x] = int(rnd(1)*23+3): next :rem formula doesn't matter for OOM
660 ? chr$(147) 
670 for x = 0 to 5
680 ? a[x]
690 next
700 ? "accept these values?"
710 get b$ :if b$="" goto 710
720 if b$="n" goto 650

 


This code is not where your problem lies.  The problem lies with some unexpected interaction with "all the arrays in the program that are filled using DATA statements."
 

 

Edited by rje
Link to comment
Share on other sites

  • 0

Thank you all for the help. I've learned a lot more that need just to fix this one issue, and that's good. When I traced the code, I released than the "NO" option was GOTOing (eventually) to the GOSUB that I was GOTOing out of. I was stacking the same GOSUB over and over. 

rje, wow! that code is very close to a subroutine of the code I can't reveal at this time 🙂 but line 650, as mentioned, is outside of this subroutine.

The code I'm working on is more spaghettified than anything I ever managed to do myself, and that's saying something. And it is over 400 lines now. Also, most lines are sequential leaving very little room to insert lines without renumbering the entire mess. I was hoping to make some small changes to fit it into x16 BASIC and the 80 character line limit. So the original author could recognize it even after the conversion. But, now that I know about the BASIC stack, I'm not sure this is an option. I should mention, the original author's version is bug free. I'm the one to blame for moving a segment into a subroutine and thinking I could get away with a GOTO. My my computer teachers always scolded me for using GOTO without fully explaining why. Now I understand, it not just about readability, GOTOs can actually break a program. 

 

 

Link to comment
Share on other sites

  • 0

You might try loading a COPY of your program into a C128 emulator, yes in 128 mode.

Then simply type 

RENUMBER

And then SAVE "PROGNAME",8

(Don't use the 128's 'DSAVE' as I think it will sneak in the start address of 128 basic).    My buddy had a 128 back in the day, and I remember him loading C64 programs into 128 mode for a quick renumber, if he got himself in trouble with so many inserted lines that he needed more space. 

I can't guaranty it will work, my buddy had a ton of utility carts on that machine so maybe he was using some special function, but I think it should work.    All the routine does is (a) go through and relink the basic lines; and (b) while it's at it, fix line number references in branch commands for GOSUB and GOTO.    I just glanced at a ROM disassembly and I don't see any error evaluating or token validation (which is true for all commodore basics, they do that during runtime).   

Do it with a copy just in case.  😬

Link to comment
Share on other sites

  • 0
7 hours ago, Snickers11001001 said:

You might try loading a COPY of your program into a C128 emulator, yes in 128 mode.

Then, simply type:

RENUMBER

And then, SAVE "PROGNAME",8

(Don't use the 128's 'DSAVE', as I think it will sneak in the start address of 128 BASIC).

SAVE does that, too.  The difference between them is that DSAVE can supply a device number (the most recently used one) and a drive number for you.

Link to comment
Share on other sites

  • 0

Yes, I remember renumbering programs from back in the day. I figured I could do that at some point;perhaps there's even modern document macro to get it done. But, I'm working with several separate code documentations that extensively references the line numbers. A renumbering program that can simultaneously 'renumber' word docs, HTML docs, text files, and emails would be a really neat trick.

Link to comment
Share on other sites

  • 0

Line numbers never phased me back in the day - but now that I've spent a lifetime coding/scripting in other languages, going back to BASIC with the line numbers felt like GAAAAH! That RENUMBER command sounds pretty awesome.

Link to comment
Share on other sites

  • 0

So, before 2020, I had not written BASIC since like 1986 or so.

Then, I wrote it weekly for the last 12 months.

And I'll tell you, BASIC's strengths extend to about 8K of code.  After that, it gets increasingly difficult to manage the variables, and it pretty much requires some sort of technical documentation.  Once you've got 20+ variables and a 16K program, a "human-readable symbol table" becomes a necessity.

There's probably one or more curve functions that together express this relationship.

I wrote a preformatter that automatically generates line numbers and handles labels and long pseudo-variables.  That let me write longer code, as in, maybe 16K to 20K.

And then its lack of block scope and functions just got to me.  The cognitive load to writing long BASIC programs is noticeable and very distracting. 

 

Edited by rje
  • Like 1
Link to comment
Share on other sites

  • 0
1 hour ago, rje said:

Once you've got 20+ variables and a 16K program, a "human-readable symbol table" becomes a necessity.

For sure.     I remember the biggest problem was you would have an idea for some neat routine, sound or graphics related.   Write up a little proof of concept program and it worked great.   But then stick it in a full sized program and that "degraded performance/coherence as function of size/complexity" function you mention would kick in and it wouldn't work right or would crawl and (especially animations) come across all 'jerky' because there was no way to time-standardize subroutines with any sort of consistency.    

But I think in the end there was a nice plus to the way this nudged us to learn at least some assembly almost by necessity.   I was one of the unlucky few that did most of this process on a Commodore Plus/4.    I never wrote a full machine code game, but when that particular model relocated BASIC to make room for the bitmap when you entered a Graphics mode, there was about a 2K area right under the color/luminances tables that wasn't used by BASIC.    I remember making some custom routines that lived in that space to do things like "scan the memory addresses for the 'even' Y coordinates in a particular range, find all the instances of specific memory value, write 0 to that location and rewrite that memory value one char segment to the right, but set a flag if the corresponding luminance for the destination is greater than 5'  (i.e., move all of my player spaceship's bullets, and since only the enemy ships were drawn with LUM6 and 7, detect the collisions!).     

Even without machine code, we came up with some wild ways to speed up BASIC.    I don't remember what the threshold was, but there was a point where if you had more than about X variables, calculations with variables initiated at the X+n'th initiated variables were so slow that the solution was to rewrite to initiate a series of "register' variables as the very first ones used by the program (i.e., A,B,C,D,E,F,G,H); put all your other scaler variables in the first single-dimension array; and put subroutine loop at line 1 that took a pointer you set just before calling it and looped through a range of your array, sticking those values into those fastest 8 variables, before returning to do the calculations or do the slow graphic commands using the A,B,C, etc., variables; and, if any of those were altered in the process, jumping to a complementary routine in line 2 that looped through commands to stick those values back in the V(i) variables array.     

Oh God, the number of times I stuck TI$="000000" and J=TI at the start and end of a bit of code so I could count Jiffies!   What an experience.    

My best friend Jack (the guy with the 128) put it best, ultimately.   He said "BASIC is a player piano, that's all"     And once you accept that and do things like pre-calculate a few thousand sine/cosine values and stick them in an array rather than trying to do those maths on the fly, you could wring a bit better performance out.  

Still, if BASIC scaled any better, I probably never would have bothered to play with assembly -- even in the hackneyed way I did it futzing around with the machine language monitor and not any sort of real assembler.    

  • Like 1
Link to comment
Share on other sites

  • 0
On 3/26/2021 at 10:47 AM, Snickers11001001 said:

Still, if BASIC scaled any better, I probably never would have bothered to play with assembly -- even in the hackneyed way I did it futzing around with the machine language monitor and not any sort of real assembler.    

I didn't do a ton with assembly in my 8-bit days. It was just a little too low level for my primitive coding skills to fully grasp at the time (though my first job after my first attempt at college did almost everything in x86 assembly, which made a lot more sense to me because it provided so many more instructions that it helped me bridge the gap between BASIC and 6502 assembly).

One thing I did do in assembly was to speed up the intro screen of a Family Feud style game on my C-128-D. It was all text mode using PETSCII graphics, but the intro / attract screen was meant to have the bottom line of text scroll from right to left to display a copyright / credits string. My first attempt in BASIC would scroll the bottom row left one character, poke the next character in at the bottom right, then repeat until the full message scrolled. It was so painfully slow to watch the characters be visibly scrolled to the left that I used some brute force to hack together a tiny ML routine to do the scroll, rather than a FOR/NEXT loop, and it suddenly went too fast to read! I had to put a pause in to get the timing just right for my tastes.

At another point, I was using BASIC to animate the intro screen which had the big FAMILY FEUD logo which it was attempting to "reverse" (if you've ever watched early Family Feud with Richard Dawson, you know what I'm talking about). It was an ugly hack (it was probably the first program I tried to write on my own, this time on a PET at school. The effect wasn't very fast, but it was fast enough, but there was a noticeable difference in speed between the top half of the screen and the bottom half. It took me a while to figure out that my problem had to do with the FOR/NEXT loops I used. The top one I did like "FOR A = 1 to X : print a character : NEXT A". The bottom one was the same, but I forgot to put the variable after NEXT (so naked "NEXT" vs "NEXT A"). That taught me two things: that the variable was optional (first time I'd encountered that) and that more BASIC text takes longer to process, even a simple white space variable name combination.

I'm sure everyone here reading probably knows these things already, just being nostalgic. 🙂

Link to comment
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.

Guest
Answer this question...

×   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.

 Share

×
×
  • Create New...

Important Information

Please review our Terms of Use