marekjm / viuavm

Parallel virtual machine designed to reliably run massively concurrent programs
https://viuavm.org/
GNU General Public License v3.0
71 stars 11 forks source link

Add two tests: #73

Closed saper closed 9 years ago

saper commented 9 years ago
marekjm commented 9 years ago

Thank you for improving the quality of machine's test suite!

I see that machine's ability to defend itself from neverending loops should be enhanced. It is able to detect some instruction-level loops, but is currently unable to find those caused by the fallthrough behaviour (as demonstrated by neverending0.asm). Good that you've already written a test for that.

saper commented 9 years ago

I made something similar to neverending0.asm by accident in my first code I wrote and it hanged. I am not sure if a function without end is legal, but it seems it kind of works. Would be interesting to check what happens with the parameters...

marekjm commented 9 years ago

Functions without an end instruction are legal. The fallthrough behaviour is there by design.

However, compiler can warn you about such functions as this functionality can be potentially dangerous. Compile with --Wmissing-end to tell the compiler to issue warnings when it finds functions without end at the end. Compile with --Emissing-end flag to make this warning fatal.

Alternatively, use --Wall to turn on all warnings, and use --Eall to make all warnings fatal.

marekjm commented 9 years ago

As to what happens with the parameters: they are retained for as long as the frame is not popped from the stack.

To call a function you must create a frame, which frame is then used to store local registers and arguments. Until the frame is popped from the stack (and, effectively, destroyed) the objects held in its registers are present in memory. Fallthrough behaviour does not cause memory leaks and is generally safe to use, apart from the infinite loop scenario.

You can launch the neverending0.asm program in debugger to see that every call generates new frame.

./build/bin/vm/asm sample/asm/functions/neverending0.asm
./build/bin/vm/vdb a.out

Commands above will let you launch a program in debugger. Use command below while inside debugger shell to inspect running program (each command is shown in monospace, commands are sent to debugger after Enter is hit):

  1. cpu.init to initialise the CPU,
  2. cpu.tick 100 to execute 100 instructions,
  3. trace to display stack trace (and see that every call has it's own frame),
  4. ^D to quit the debugger,

Your output should look like this (I used cpu.tick 40 for shorter log):

message: running "a.out"
bytecode size: 101
>>> cpu.init
>>> cpu.tick 40
byte 51 (0x33) at 0x2206313: ress global
byte 56 (0x38) at 0x2206318: frame 1 16
byte 67 (0x43) at 0x2206323: paref 0 1
byte 78 (0x4e) at 0x220632e: call 1 main
byte 23 (0x17) at 0x22062f7: frame 0 2
byte 34 (0x22) at 0x2206302: call 0 one
byte 0 (0x0) at 0x22062e0: istore 1 42
byte 11 (0xb) at 0x22062eb: print 1
42
byte 17 (0x11) at 0x22062f1: izero 0
byte 23 (0x17) at 0x22062f7: frame 0 2
byte 34 (0x22) at 0x2206302: call 0 one
byte 0 (0x0) at 0x22062e0: istore 1 42
byte 11 (0xb) at 0x22062eb: print 1
42
byte 17 (0x11) at 0x22062f1: izero 0
byte 23 (0x17) at 0x22062f7: frame 0 2
byte 34 (0x22) at 0x2206302: call 0 one
byte 0 (0x0) at 0x22062e0: istore 1 42
byte 11 (0xb) at 0x22062eb: print 1
42
byte 17 (0x11) at 0x22062f1: izero 0
byte 23 (0x17) at 0x22062f7: frame 0 2
byte 34 (0x22) at 0x2206302: call 0 one
byte 0 (0x0) at 0x22062e0: istore 1 42
byte 11 (0xb) at 0x22062eb: print 1
42
byte 17 (0x11) at 0x22062f1: izero 0
byte 23 (0x17) at 0x22062f7: frame 0 2
byte 34 (0x22) at 0x2206302: call 0 one
byte 0 (0x0) at 0x22062e0: istore 1 42
byte 11 (0xb) at 0x22062eb: print 1
42
byte 17 (0x11) at 0x22062f1: izero 0
byte 23 (0x17) at 0x22062f7: frame 0 2
byte 34 (0x22) at 0x2206302: call 0 one
byte 0 (0x0) at 0x22062e0: istore 1 42
byte 11 (0xb) at 0x22062eb: print 1
42
byte 17 (0x11) at 0x22062f1: izero 0
byte 23 (0x17) at 0x22062f7: frame 0 2
byte 34 (0x22) at 0x2206302: call 0 one
byte 0 (0x0) at 0x22062e0: istore 1 42
byte 11 (0xb) at 0x22062eb: print 1
42
byte 17 (0x11) at 0x22062f1: izero 0
byte 23 (0x17) at 0x22062f7: frame 0 2
>>> trace
__entry/0(): frame at 0x2206d60
 main/1(["a.out"]): frame at 0x2208110
  one/0(): frame at 0x2207fc0
   one/0(): frame at 0x22084f0
    one/0(): frame at 0x22081f0
     one/0(): frame at 0x22084c0
      one/0(): frame at 0x2209170
       one/0(): frame at 0x2209750
        one/0(): frame at 0x2209640
>>>

You can use help in debugger to get a list of available commands.

saper commented 9 years ago

Ok! This should maybe go into the docs:)

one format question: you wrote "see that every call generates new frame." - during call nothing is changed, only frame instruction generates new frame, correct?

marekjm commented 9 years ago

Seems like you are already pretty familiar with the machine! You're right, only frame instructions create new frames. The "see that every call generates new frame." was only a language shortcut.

One thing should be clarified though - the call instructions does in fact change something (besides the execution pointer, of course).

It must be noted that frame instructions create new frames in a temporary frame slot. It is the call instruction that actually pushes frames on stack, and resets the temporary frame slot (sets it to point to 0, marking it as ready to accept new frames). In similar manner, param instructions operate on the temporary frame not on last frame on the stack. This is intended to prevent accidental frame modifications after the call has been issued - once the frame has been pushed on the stack it must remain unchanged (for debugging purposes).