LoginLogin
Nintendo shutting down 3DS + Wii U online services, see our post

Strange Behaviour with PRGEDIT

Root / SmileBASIC Bug Reports / [.]

amihartCreated:
I don't know if this is a bug or not, it really depends on what the developers had intended, but I do find it to be some pretty strange behaviour which I don't quite understand the purpose of. Let's say we have code executing in a given slot, we'll call it slot A. We also will be working with another different slot, let's call it slot B. Slot A:
PRGEDIT B
PRGEDIT A
The first line of the code above will execute just fine. We're allowed to edit the code in other slots. But the second line of code, such that we want to edit the slot our code is running in, crashes with an "Illegal function call error". Let's look at some more code. Slot A:
USE B
GOSUB STR$(B)+":@THIS"
Slot B:
@THIS
PRGEDIT A
PRGEDIT B
Using "GOSUB" to another slot has the same effect as the first code. When we are in Slot B, "PRGEDIT A" works fine, since we're editing another slot, but "PRGEDIT B" crashes because again, we're trying to edit our own slot, the slot the code is running in, and this causes a crash. So, at first glance it seems to me the developers don't want code being able to read and edit itself. I have no clue why this wouldn't be allowed, but that's what it seems. However, we haven't tried one thing yet. What if we use "GOTO" instead of "GOSUB"? Slot A:
USE B
GOTO STR$(B)+":@THIS"
Slot B:
@THIS
PRGEDIT A
PRGEDIT B
Surprisingly, this code throws no errors. If you "GOTO" another slot, modifying your own slot becomes perfectly valid. This also works from Direct Mode. I can type in the GOTO statement and execute code that way from Direct Mode, and that code now is allowed to use PRGEDIT on itself. However, you have to attempt to run the code first, let it crash, and then execute from Direct Mode, since it needs to load the symbol table into memory first before GOTOs will work from Direct Mode. Let's take a look at this. Put this code into Slot 0:
@MAIN

PRGEDIT 0
VAR I
FOR I=0 TO PRGSIZE()-1
  PRINT PRGGET$();
NEXT
Try to run the code normally. It will crash with an error when it hits "PRGEDIT 0" because that's not allowed. However, by running the code we've now loaded the symbol table into memory. So go to Direct Mode and type:
GOTO "0:@MAIN
The code will now execute just fine. It will read its own code and then print out to the screen. But, there's more interesting behaviour than this. The code that SmileBASIC interprets is not the code that is within the slot. SmileBASIC first loads the code from the slot into another location in memory and then executes it from there. We can see this using this code:
@MAIN

PRGEDIT 0
PRGDEL -1

VAR I=0
WHILE BUTTON()!=#X
  PRINT I
  INC I
WEND
So the WHILE loop of this code is simple, it just continues to count upwards forever until you press X. But look what's above the WHILE loop. Those two lines of code will delete all of the contents in Slot 0. But our code is stored within Slot 0. So at first you'd think, the code should delete itself, and then stop executing immediately, right? It will never reach the WHILE loop. But try it. Again type "RUN" in Direct Mode to get the error, then type the same GOTO statement mentioned before. The code will start counting away. It successfully executes, meaning it reached the WHILE loop. Now press X to stop the program. So, it reached the WHILE loop. Does that mean the program failed to delete itself? Open slot 0 now. You will find that it is empty. The code deletes itself successfully, but since it's not actually being executed from the slot itself but from another buffer in the 3DS's memory, let's call this "execution memory", it can continue executing even though it's deleted itself. So code can delete itself while continue to run. Using this knowledge, we can completely lift the restrictions from a program entirely while running it normally from Direct Mode like so: Slot A
1. Inject into Slot B so that it:
- a. Deletes itself.
- b. Jumps to line 4.
2. Use Slot B. 
3. Jump to Slot B.
4. Some label here to signify the start of your code.
Here's an example:
'Detect which slot we're in.
VAR I
FOR I=0 TO 3
  IF PRGNAME$()==PRGNAME$(I)
    BREAK
  ENDIF
NEXT

'Define our slots.
VAR SLOT_A=I
VAR SLOT_B=(SLOT_A+1) MOD 4

'Inject code into Slot B.
PRGEDIT SLOT_B
PRGDEL -1
PRGSET "@FIX"
PRGSET "PRGEDIT "+STR$(SLOT_B)
PRGSET "PRGDEL -1"
PRGSET "GOTO "+CHR$(34)+STR$(SLOT_A)+":@MAIN"+CHR$(34)

'Execute the injected code.
USE SLOT_B
GOTO STR$(SLOT_B)+":@FIX"

'The main body of our program.
@MAIN

'Print the code in the slot of the currently running program.
PRGEDIT SLOT_A
VAR I
FOR I=0 TO PRGSIZE()-1
  PRINT PRGGET$();
NEXT

'Delete itself. 
PRGDEL -1
What this will do, when loaded into any given slot, is first lift the restrictions of "PRGEDIT", then it will read its own source code and print it to the screen, and then it will delete itself so the program you just executed will disappear from the slot (it will also clear the slot it used to lift the restrictions). At first to me it seemed like SmileBASIC devs seemingly don't want code that can read and modify itself at runtime. Why else would you disallow code from reading and writing to its own slot? But then again you can easily lift these restrictions with a GOTO statement so I don't know why they're there in the first place. In fact on their website it states under PRGEDIT that "Specifying the SLOT currently running will give an error". But it doesn't, that is, if you entered that slot with a GOTO statement. I don't know if this is a bug in the sense they don't want you to be able to use PRGEDIT on a program's own slot and this is a way around it, or if there's some good technical reason to why it doesn't initially work but is acceptable after GOTO statements. Either way it's definitely some strange behaviour and I thought it was worth pointing out.

I don't find this surprising at all. Being able to edit code that's currently being run would open up a huge can of worms. Here's an example loaded into slot 0, with line numbers for clarity:
 1 PRINT "HELLO"
 2 GOSUB @FOO
 3 PRINT "WORLD"
 4 END
 5
 6 @FOO
 7 PRGEDIT 0,2
 8 PRGINS "PRINT 123"
 9 RETURN
The question here is, where does @FOO return to? Typically, GOSUB @FOO would push a pointer to the stack which points directly after it, then jump to @FOO. Then, when RETURN is reached, it pops that pointer off the stack and jumps back to right after GOSUB @FOO, where it can execute the next instruction. For simplicity, let's say it works with line numbers, so it pushes 3 to the stack. Then, stepping through it, this is what happens:
  • "HELLO" is printed.
  • 3 is pushed to the stack, and the program jumps to @FOO.
  • The line "PRINT 123" is inserted before line 2, shifting everything below it down a line. Now the program looks like this:
     1 PRINT "HELLO"
     2 PRINT 123
     3 GOSUB @FOO
     4 PRINT "WORLD"
     5 END
     6
     7 @FOO
     8 PRGEDIT 0,2
     9 PRGINS "PRINT 123"
    10 RETURN
  • 3 is popped off the stack, so it returns to line 3... which is now GOSUB @FOO again. Uh oh.
  • And it just keeps going in an endless loop, inserting PRINT 123 forever until it inevitably runs out of space and crashes.
And that's just one of the many ways things could go horribly wrong. Say you call FOO, which calls BAR, which deletes FOO and then returns. Now what? Of course, you could say that none of this is a problem in the end because SmileBASIC copies the program to another location before running it. But then, not only does this defeat the point of editing the current slot, allowing you to do it would just mislead people into thinking it works when it doesn't. It might be useful for making confusing programs, but I don't think that's the usecase SmileBoom had in mind.

There are cases where it would be useful to edit the current slot (and besides, Smileboom didn't do a good job of preventing it. It would be better to have more consistent behavior rather than relying on bugs) This library: http://smilebasicsource.com/page?pid=1053 has a loader program to generate the actual code that will run. Without being able to edit the current slot, it forces you to waste TWO slots to use it. EDIT: Ok here's why this happens. PRGEDIT A is blocked whenever slot A is in the "slot call stack"*. Using EXEC, calling a function in another slot, or using GOSUB to jump to another slot will all add the current slot (and current position in the code) to the slot call stack, so SB knows where to return. A slot is returned from when: - The end of the program or END is reached (only if EXEC was used, otherwise the program just stops) - The function or GOSUB is returned from. EXEC is special because if you use it to execute a slot that is already in the slot call stack, that slot's entries will be removed (along with anything below them (including things in the normal call stack)). This is because EXEC will recompile that slot (SB compiles programs into bytecode rather than interpreting the text), so it would be impossible to return to it. (USE also recompiles the slot and removes its call stack entries, but doesn't do anything else.) This will loop forever without causing a stack overflow: Slot A:
EXEC B
Slot B:
EXEC A
(Starting in slot A) 1: EXEC B adds A to the slot call stack, then jumps to slot B 2: EXEC A removes A from the call stack, then adds B and jumps to slot A 3: EXEC B removes B from the call stack, then adds A and jumps to slot B ... The slot call stack never has more than one item. Anyway, the reason that PRGEDIT works with GOTO is that GOTO doesn't update the slot call stack. You can't return from a GOTO, so there would be no reason to store the location it was called from. Oh, another neat thing I found: Slot 0:
USE 1
GOSUB @TEST
PRINT "HI"
STOP

@TEST
 GOTO "1:@T"
Slot 1:
@T
RETURN
The RETURN actually works. (Note: The "slot call stack" is probably just mixed in with the normal call stack. However, the items are special... If you call a function in slot 0 (adding an entry to the call stack), then use GOTO to jump to slot 1, PRGEDIT 0 will work even though an item from slot 0 is in the call stack. PRGEDIT is only blocked by call stack entries which are added when jumping BETWEEN slots. I'm just using "slot call stack" to specifically talk about these items.) Also all the information here is just what I *think* is happening, and it might not be true.