Run
The (static) executable is run by the “TlMachine” class, using the previously loaded configuration to create the VM.
graph LR; exe{{"Executable"}} dc{{"Data Controller"}} inv{{"Invoker"}} fn{{"Run arguments"}} machine{{TlMachine}} fn --> machine exe --> dc dc --> machine inv --> machine --> top[Run from top]
Overview
Relevant functions & classes:
TlMachine
-- hark_lang/machine/machine.pyExecutable
-- hark_lang/machine/executable.pyController
-- hark_lang/machine/controller.pyInvoker
-- hark_lang/executor/thread.pyInstruction
-- hark_lang/machine/instructionset.py and hark_lang/machine/instruction.py
Vaguely speaking, “running” a program follows this logic:
stateDiagram Eval : Eval Eval : Run instruction at current Instruction Pointer (IP) Step : Inc Step : Increment IP [*] --> Eval Eval --> Step Step --> Eval : Not stopped Step --> [*] : Stopped
To run the executable, the VM simply executes instructions until it cannot anymore. Reasons it may stop:
- returning from the entrypoint function
- waiting for another thread to finish
Machine instructions can do several things:
- change the instruction pointer (control flow -- branches, jumps)
- push/pop the data stack
- halt the machine (e.g. to wait for a thread)
- perform actions with external side effects (new thread, write to stdout)
Note: Next we will go through some bytecode. Each instruction is defined in instructionset.py and implemented in machine.py.
Start Top Level Thread
The Invoker instance calls TlMachine.run
is to begin execution at main
:
| ;; #3:main:
| 28 | PUSHV 1
| 29 | PUSHB compute
| 30 | CALL 1
| 31 | RETURN
Instructions:
-
PUSHV 1
-- push the literal value1
(integer 1) onto the data stack. -
PUSHB compute
-- push the value bound tocompute
onto the stack (in this case, the compute function). -
CALL 1
-- call a function with one argument. The top value on the stack is the function, and subsequent values are arguments. -
RETURN
-- (aftercompute
returns) return from the function, ending the program in this case.
The CALL
instruction creates a new Activation Record (like a stack frame)
and sets the IP to the location of compute
.
Compute
Prev: Top Level
Next: New Thread
Now we evaluate compute(1)
:
| ;; #2:compute:
| 10 | BIND x
| 11 | POP
| 12 | PUSHB x
| 13 | PUSHB foo
| 14 | ACALL 1
| 15 | BIND a
| 16 | POP
| 17 | PUSHB x
| 18 | PUSHB bar
| 19 | CALL 1
| 20 | BIND b
| 21 | POP
| 22 | PUSHB b
| 23 | PUSHB a
| 24 | WAIT 0
| 25 | PUSHB +
| 26 | CALL 2
| 27 | RETURN
Interesting steps:
-
BIND x
-- bind the value on the top of the stack (without popping it) tox
. -
ACALL 1
-- call a function asynchronously with one argument. Again, the top value on the stack is the function (foo
), and subsequent values are arguments. This uses the Invoker to start a new thread with the given function and arguments. -
BIND a
-- bind the ACALL result toa
(it will be a Future object). -
WAIT 0
-- wait for the top object on the stack to resolve (assuming it is a Future).
graph LR; wait["WAIT for foo(x)"] --> check{Resolved?} check -->|No| stop[Save continuation and stop] check -->|Yes| cont[Get the value and continue]
The simple “Yes” case can be visualised:
gantt title foo(x) completes before Wait dateFormat YYYY-MM-DD axisFormat section main() Run to end :a1, 2020-01-01, 30d section foo(x) Run foo(x) :f1, 2020-01-10, 5d
In this case, foo(x)
finishes in the time it takes Thread 0 to run the 10
instructions between ACALL
and WAIT
.
Alternatively:
gantt title foo(x) takes longer dateFormat YYYY-MM-DD axisFormat section main() Run until Wait :a1, 2020-01-01, 18d Resume :after f1, 5d section foo(x) Run foo(x) :f1, 2020-01-10, 15d
In this case, foo(x)
takes longer and Thread 0 waits for it to continue. While
waiting, the underlying Python thread is killed.
Each Future has a list of “continuations”, which are records of threads which can be picked up and resumed at a later time. When the Future does resolve, it can then use the Invoker to resume each thread.
New Thread
ACALL
creates a thread context for foo(x)
- data stack, future and IP - and
uses the Invoker to start the thread.
| ;; #F:foo:
| 0 | PUSHB foo
| 1 | CALL 1
| 2 | RETURN
CALL 1
pops foo
and one argument from the stack, and then calls foo
directly (after converting the argument to a Python type). The result is
converted to a Hark time and pushed onto the stack as the return value.
The Future associated with this thread is then resolved to that value, and any waiting threads are resumed.
Finish
Once foo(x)
has finished, and compute(1)
is ready to RETURN
, it retrieves
the caller IP from its activation record, and simply jumps there. The activation
record is deleted.
Finally, we return from main()
-- all threads have “finished”, and the result
is returned to the CLI to be printed.
The end. Here’s a spaceship.
`. ___
__,' __`. _..----....____
__...--.'``;. ,. ;``--..__ .' ,-._ _.-'
_..-''-------' `' `' `' O ``-''._ (,;') _,'
,'________________ \`-._`-','
`._ ```````````------...___ '-.._'-:
```--.._ ,. ````--...__\-.
`.--. `-` ____ | |`
`. `. ,'`````. ; ;`
`._`. __________ `. \'__/`
`-:._____/______/___/____`. \ `
| `._ `. \
`._________`-. `. `.___
SSt `------'`