Lhogho
0.0.027
|
When Lhogho compiles Logo source it uses code generation patterns. I.e. it recognizes specific patterns in the user program and generates code according to some predefined templates.
Templates are extremely processor-specific. Because currently Lhogho compiles for i386 processors (like Pentium family, for example) the code presented in this page will use i386 assembly mnemonics.
Lhogho tries to be consistent in respect to how processor registers are used. The list below describes the usage of some of the registers:
The stack is used to keep a variety of data - function parameters, static pointer, dynamic pointer, local variables, and information about repeat
commands.
The stack structure inside user-defined functions is:
Offset | Type | Contents | Macro (vars.h) |
EBP+16 | atom_t | 1st parameter passed from caller | BASE_OFFSET_PARAMS+4 |
EBP+12 | atom_t | 2nd parameter passed from caller | BASE_OFFSET_PARAMS |
EBP+8 | int | number of parameters | |
EBP+4 | int | Returned address (generated by the CALL instruction) | none |
EBP | int | Dynamic frame pointer (old EBP). Used to access the stack frame of the calling procedure. | none |
EBP-4 | int | Static frame pointer (old ESI). Used to access the stack frame of the parent procedure. | BASE_OFFSET_STATIC |
EBP-8 | atom_t | The var atom of the procedure. | BASE_OFFSET_PARENT |
EBP-12 | atom_t | Repeat chain | BASE_OFFSET_REPEATCHAIN |
EBP-16 | int | Value for TEST , IFTRUE , IFFALSE | BASE_OFFSET_TEST |
EBP-20 | atom | list of local variables created at runtime | BASE_OFFSET_LOCALS |
EBP-24 | atom | 1st local variable | BASE_OFFSET_LOCALS-4 |
EBP-28 | atom | 2nd local variable | BASE_OFFSET_LOCALS-8 |
Lhogho generates code only for the user-defined functions/commands/operators (i.e. those which are defined in Logo source code). Primitives are defined in C and are precompiled into the compiler. The main Logo program is also considered as a user-defined function with one exception -- the repeat chain which is pushed in the stack is not the unbound atom, but an atom which has REPCOUNT = -1.
Logo: to name... local "local1 local "local2 : {command1} {command2} {command3} : end | Assembler: name: push ebp ; prologue mov ebp,esp push esi push "(unbound)" ; repeat chain call use push 2 ; initial value for TEST mov eax,(unbound) ; default result push "(unbound)" ; initialization of call use ; local variables push "(unbound)" call use {command1} ; function body {command2} {command3} mov ebx,0 exit: call deuse ; finalization of call deuse ; local variables pop edx ; value for TEST call deuse ; repeat chain pop esi ; epilogue mov esp,ebp pop ebp ret |
The generated code for each user-defined functions contains prologue, body and epilogue. The prologue initializes the function by setting the frame and the stack pointers, while the epilogue reverts them to their original values. Both the prologue and the epilogue do not correspond to any part of the Logo source.
The body is a sequence of calls to commands.
Exiting from the definition is done by a jump to the exit
label.
The code generated for a command-call is composed of two fragments - an expression-call (i.e. the command is treated as expression); and result verification code.
Logo command param1 param2 | Assembler {expr} push {source} call rt_cmdchk cmp [eax+OFFSET_ID],ERROR_ID je exit |
{Expr} is a fragment of code where the Logo command-call is compiled as if it is an expression. The result of {expr} is stored in the stack and rt_cmdchk function checks whether it is legal for a command.
If the result of {expr} is:
rt_cmdchk
returns the same atom;rt_cmdchk
returns the same atom;rt_cmdchk
deuses the atom and returns ERROR_UNUSED_VALUE atom.If rt_cmdchk
returned an error atom, then execution jumps to the exit
point of the function.
rt_cmdchk
accepts two inputs. The first one is the result of the command treated as expression, and the second one, i.e. {source}
, is the source code corresponding to the command. This source code is used to set the error position if needed. It is also used for the runtime trace mode.
The code generated for a function-call (or an operator-call) is similar to the one generated for a command-call - only the verification code is different.
Logo: ... (functon param1 param2) ... | Assembler: {expr} push {source} call rt_funchk push eax |
{Expr} is a fragment of code where the Logo function-call is compiled as if it is an expression. The result of {expr} is stored in the stack and rt_funchk function checks whether it is legal for a function or an operator.
If the result of {expr} is:
rt_funchk
returns the same atom;rt_funchk
returns ERROR_UNUSED_VALUE atom;rt_funchk
returns the same atom.The overall outcome of the code for functions (if there is no error) is the the final value is pushed in the stack.
If rt_funchk
returned an error atom, then execution does not jump to the exit
point of the function.
rt_funchk
accepts two inputs. The first one is the result from the function, and the second one is the source code corresponding to the function-call. This source code is used to set the error position.
Expression are massively generated. Each call to a function or a command is based on expressions. The code generated for an expression leaves the result in the stack. The code generated for an expression depends on whether the expression is a constant or it has functions or operators.
Expressions which are contants (i.e. numbers, word-constans and list-constants) are pushed directly into the stack. Their reference count is incremented with use.
Logo: const | Assembler: push const call use |
Expression which uses a primitive command, function or operator is compiled into three fragments: (1) all inputs are compiled as standalone expressions (i.e. constants or function-calls), (2) code for the actual call of the primitive, and (3) code to release all inputs.
Logo: primitive input1 input2 | Assembler: {expr} {expr} call {primitive} call deuse call deuse push eax |
The two {expr} fragments stand for the code generated for the two inputs. Their results appear pushed in the stack. {Primitive} is the address of the primitive function/command/operator. The result of this call is stored in EAX regiter. For each input there is a call to deuse() function which removes it from the stack. deuse() does not modify EAX register, thus the last instruction is to push its contents in the stack. This value is the result of the call of the primitive.
Variadic call is a call which number of inputs could vary. Such calls are compiled in the same way as non-variadic ones, except that the number of inputs is explicitely pushed before the call and popped after it.
Logo: (function input ... input) | Assembler: : push {n} call {function} pop edx : |
The constant {n} is an integer number - the actual number of inputs.
Expression which uses a user-defined command, function or operator is compiled as the primitive expression (see (a)) expect that a static link is calculated and stored in ESI
register before the call.
A user-defined function may be recompiled (thus changing its address) or could be still uncompiled (i.e. no address is defined for it) during the compilation of a call to this function. To resolve this problem in realtime the function is called through its address stored in function's descriptor.
Logo: userfunc input1 input2 | Assembler: {expr} {expr} {static} call [{userfunc}] call deuse call deuse push eax |
Some functions require access to system defined variables. For example, print
, show
and type
use the values of printdepthlimit
, printwidthlimit
and fullprintp
. Similarily, equalp
, notequalp
, memberp
and member
use the calue of caseignoredp
.
The values of such system variables are explicitely pushed just before the actual call. If the call is variadic, the system variables are pushed after the number of inputs.
Logo: (print input input...) | Assembler: {expr} : {expr} push {n} push {value of system var1} call use : push {value of system var1} call use call {print} call deuse : call deuse pop edx call deuse : call deuse |
Whenever Lhogho need to generate code to access a user-defined variable or prepare a static link for user-defined function, command or operator, it is needed to provide mechanism to find the appropriate context (i.e. stack frame). If the accessed object is in the current context, then it could be accessed directly through EBP
register.
If it is in some upper level of lexical scope hierarchy, then the access is done by calculating the context in ESI
register.
EBP
register is used. If ESI
is required, then: mov esi,ebp |
ESI
register to hold the correct stack base pointer: mov esi,[ebp-4] |
ESI
register to hold the correct stack base pointer: mov esi,[ebp-4] mov esi,ss:[esi-4] |
ESI
register to hold the correct stack base pointer: mov esi,[ebp-4] mov esi,ss:[esi-4] \ : ) N-1 mov esi,ss:[esi-4] / |
There are two types of variables - primitive (system) and user-defined. They are accessed in different ways, because primitive variables are global and their values are in fixed memory locations. User-defined variables have their values stored in the stack.
System (i.e. primitive) variables are created during the initialization of the compiler. They are stored in the globals variable. The value of each system variable is at a memory address which is known at compile time. Thus the code for referencing a system variable is:
push [{value}] call use |
where {value} is the address whether the value of the variable is stored.
Code generated to access the value of a variable depends on whether the variable is in the current context (in this case EBP
is used) or in an upper context (ESI
is calculated and used). The offset of the variable within context is fixed and does not depend on from what context the variable is being accessed.
The code to access a variable pushes its value in the stack and also returns it in EAX
register:
push [ebp-{offset}] call use |
{static} push [esi-{offset}] call use |
The code for {static} fragment is the code which calculates the static link to the context frame of the variable.
The MAKE
command is a special case and it is not compiled as an ordinary command. This is done in order to maintain higher performance and to avoid some difficulties associated with assignments (like lvalues and rvalues).
Depending on the arguments MAKE
can be compiled in one of these ways:
When the first input of MAKE
is a word-constant, then Lhogho can assist in finding the location of the variable's value during compile time. This contributes to a higher performance in run-time and is called direct MAKE.
Then Lhogho finds a variable it can be in the same context level or in an upper one. If the context level is the same, the generated code is:
Logo: make "{name} {expr} | Assembler: {expr} push [ebp-{offset}] call deuse mov eax,[esp] mov [ebp-{offset}],eax push {source} call rt_makechk cmp [eax+OFFSET_ID],ERROR_ID je exit |
If the variable is defined in an upper context then code for a static link is generated and the variable is referenced through ESI
register.
Logo: local "{name} to ... to ... to ... make "{name} {expr} end end end | Assembler: {expr} {static} push [esi-{offset}] call deuse mov eax,[esp] mov [esi-{offset}],eax push {source} call rt_makechk cmp [eax+OFFSET_ID],ERROR_ID je exit |
{Expr} is the code generated for the second input of MAKE
. The execution of {expr} should put in the stack the new value of the variable.
There is no code for the first input, because it is used explicitely. {Static} is the code which calculates the static link to the context frame of the variable. The value of {offset} is the offset of the variable related to the frame pointer (i.e. EBP
or ESI
). The offset is calculated at compile-time.
The verification of the result is done with rt_makechk which is similar to rt_cmdchk.
When a new value is assigned to a variable its old value is deused.
The output
command is compiled in a special way by Lhogho, because its execution does not go in the normal way. The generated code for output
moves the input of the command in EAX
register and jumps directly to the exit label.
Logo: output {value} | Assembler: {value} mov ebx,1 pop eax jmp exit |
When Lhogho is compiled in ADVANCED mode and when the -Zrt compiler option is used, then the generated code includes instructions to dump the source code of output
command.
Logo: output {value} | Assembler: {value} push {source} call rt_dump mov ebx,1 pop eax jmp exit |
There are two cases of if
command - with two and with three inputs. They are compiled in similar, but yet different ways.
The beginning of the generated code is the same. After the code for the condition, there is a check for whether the result of the condition expression is true
or false
. The result of rt_boolchk is in EAX
register, which could be either the true atom, the false atom or an error atom.
Logo: if {cond} {then} | Assembler: {cond} call rt_boolean cmp [eax+OFFSET_ID],ERROR_ID je exit cmp eax,FALSE je else then: {then} else: mov eax,{unbound} |
If the if
command has three parameters, then the generated code is:
Logo: if {cond} {then} {else} | Assembler: {cond} call rt_boolean cmp [eax+OFFSET_ID],ERROR_ID je exit cmp eax,FALSE je else then: {then} jmp next else: {else} next: mov eax,{unbound} |
The repeat
command is implemented in a special way -- it uses the repeat chain to keep track of the number of left and passed repetitions. The repeat chain is a list located in the local stack - it is like a system-defined local variable. The elements of the list are integer atoms. The actual 64-bit number is split into two 32-bit numbers - one of them is left for repcount
, the other is used to count down the number of repetitions left. These two values could be accessed with REPCOUNT and :REPLIMIT macros.
The first element of the repeat chain corresponds to the current repeat
. The second element is for the immediate outer repeat
. The last element is for the outer-most repeat
.
When a new repeat
is initiated by rt_repeat_enter, a new atom is added to the beginning of the repeat chain. When a repeat is completed by rt_repeat_exit then the first element of the chain list is removed.
There are two cases of repeat
command - when the number of repetition is known at compile time, and when it is expression to be evaluated at run-time.
When the repetition count is known, the generated code is:
Logo: repeat {const} {block} | Assembler: push {const} push ebp call rt_repeat_enter repeat: {block} mov eax,[ebp+REPEATCHAIN] mov eax,[eax+CAR_OFFSET] inc [eax+REPCOUNT] dec [esp+REPLIMIT] jnz repeat push ebp call rt_repeat_exit |
If the number of repetitions is 0, then no code for repeat
command is generated.
When the repetition count is not known - i.e. it is an expression - then the generated code is:
Logo: repeat {expr} {block} | Assembler: {expr} call rt_repchk cmp [eax+ID],ERROR_ID je exit push eax mov eax,[eax+REPLIMIT] call deuse cmp eax,0 je skip push eax push ebp call rt_repeat_enter repeat: {block} mov eax,[ebp+REPEATCHAIN] mov eax,[eax+CAR_OFFSET] inc [eax+REPCOUNT] dec [esp+REPLIMIT] jnz repeat skip: push ebp call rt_repeat_exit |
The forever
command is implemented similarily to repeat
-- it uses the repeat chain to keep track of the number of passed repetitions. The generated code is:
Logo: forever {block} | Assembler: push ebp call rt_forever_enter forever: {block} mov eax,[ebp+REPEATCHAIN] mov eax,[eax+CAR_OFFSET] inc [eax+REPCOUNT] jmp forever |
The for
command is implemented in a way similar to repeat
. However, for
is a variable generating command - it creates a local variable with name given as the first input of for
. This name must be a word literal.
The second input is a list constant with two or three expressions. The code of these expressions is placed at the beginning of the compiled code for for
.
First the code of the first expressions in {limits} is pushed. It pushes in the stack the initial value of the control variable. This value is then dupicated and assigned "manually" to the control variable in a way similar to make
but without result checking.
The the code for the second expression in {limits} is generated, followed by the code of the third expression. If the third expression is missing, then generate code that pushes the :UNBOUND value.
The prologue code for for
is the same as the one for repeat
. The only difference is that the check function rt_forchk
not only checks the limits but also calculates and sets the step if it is not provided.
The epilogue code for for
is the same as the one for repeat
. This it uses the same function rt_repeat_exit
to fix the repeat chain.
Logo: for {var} {limits} {block} | Assembler: {limits.1} pop eax push eax push eax push [ebp-{offset}] call deuse mov eax,[esp] mov [ebp-{offset}],eax {limits.2} {limits.3 or unbound} push {addr of ste var's value} push {source} call rt_forchk cmp [eax+ID],ERROR_ID je exit push eax mov eax,[eax+REPLIMIT] call deuse cmp eax,0 je skip_for push eax push ebp call rt_repeat_enter repeat: {block} mov eax,[ebp+REPEATCHAIN] mov eax,[eax+CAR_OFFSET] inc [eax+REPCOUNT] dec [esp+REPLIMIT] jnz repeat skip_for: push ebp call rt_repeat_exit |
The while
and until
commands are compiled in a way to check the condition at the beginning of every loop. The code for the condition leaves the result in register EAX
. The only difference between the code for while
and for until
is in one cmp
instruction and in the label names. The generated code is:
Logo: while {cond} {block} until {cond} {block} | Assembler: while: {cond} push {source} call rt_whlchk cmp [eax+ID],ERROR_ID je exit push eax mov eax,[eax+INT] call deuse cmp eax,0(for while) or 1(for until) je skip {block} jmp while skip: push (unbound) call use pop eax |
The do.while
and do.until
commands are compiled in a similar way to while
and until
except that the block of commands is placed before the check of the condition. Thus the commands are executed at least once. The generated code is:
Logo: do.while {block} {cond} do.until {block} {cond} | Assembler: do.while: {block} {cond} push {source} call rt_whlchk cmp [eax+ID],ERROR_ID je exit push eax mov eax,[eax+INT] call deuse cmp eax,0(for while) or 1(for until) je skip jmp while skip: push (unbound) call use pop eax |
The catch
command/function is compiled in a way to check the result of some executed commands and mask any exceptions if the tags match (the tag of the catch
and the tag of the throw
).
The code for catch
has a special feature - the exit address is temporarily changed to point to the catch epilogue. This ensures that the epilogue is the exit point of any condition happening inside the catch's body.
The generated code for procedural catch
is:
Logo: catch {tag} {block} | Assembler: jmp catch exit: jmp exit_catch catch: {block} exit_catch: push eax {tag} call rt_catchchk push eax |
The generated code for functional catch
is:
Logo: catch {tag} {block} | Assembler: jmp catch exit: jmp exit_catch catch: {block} exit_catch: {tag} call rt_catchchk push eax |
The goto
command is compiled in two different ways depending on wheter the target tag is found at compile time. If it is found, then the generated code is:
Logo: goto {tag} | Assembler: jmp {tag} |
If the target tag is an expression, the target is calculated at run-time. The value of the expression is a name of a local variable of type VAR_TYPE_TAG which VALUE is an integer atom. This atom is the target address relative to the beginning of the memory block of compiled code.
The generated code for goto
which is expression is:
Logo: goto {tag} | Assembler: push {tag source} {tag} push {parent} push {static link} call rt_goto pop edx pop edx call deuse call deuse cmp [eax+ID],ERROR_ID je exit push eax mov eax,[eax+INT] call deuse add eax,{mem block addr} jmp eax |
The iftrue
and iffalse
commands test the input value of the latest executed local test
command. The test
command write 0 or 1 in the local stack at location [EBP+BASE_OFFSET_TEST]. 0 means the input of latest test
was false
. 1 means it was true
.
In procedure prologue the value 2 is written in this location, thus initially it is neither true
nor false
.
Both commands iftrue
and iffalse
are compiled in the same way. The only difference is a constant to test with.
The generated code for iftrue
is:
Logo: iftrue {block} | Assembler: cmp [ebp+BASE_OFFSET_TEST],1 jne skip {block} skip: |
The generated code for iffalse
is:
Logo: iffalse {block} | Assembler: cmp [ebp+BASE_OFFSET_TEST],1 jne skip {block} skip: |
When an empty Logo function is made external, this means that the body of the function is compiled in another language. The main issue is that the external procedure expects parameters to be in the processor's native data format.
Externalization of a Logo function builds a wrapper that converts Logo data for parameters into native format, then executes the external function and at the end converts its result back into Logo atom.
The wrapper clears a global error flag and checks it after the call of the external function. This flag is set by internal Lhogho function (if such is called from the external non-Lhogho function).
The generated code for wrapper of external functions is:
Logo: to func :a :b end external "func [i4 func i4 i4] :lib | Assembler: push ebp mov ebp,esp ; clear error flag mov [error_flag],0 push [ebp+{ofs}] ; push param 2 call atom_to_i4 ; convert atom to native {push native} ; push param in native format push [ebp+{ofs}] ; push param 1 call atom_to_i4 ; convert atom to native {push native} ; push param in native format call {external_func} ; check error flag cmp [error_flag.id],0 je $ok mov eax,[error_flag] jmp exit ok: {push native} ; push result in native format call i4_to_atom ; convert native to atom exit: mov esp,ebp pop ebp ret |
Pushing datum in native format is compiled in different ways depending on the size and the type of the datum. Integer values up to 32 bits (signed, unsigned, cardinal, enumerations, etc) are returned in EAX
register and are pushed with this code:
Assembler: push eax |
Integer values larger then 32 bits and up to 64 bits are returned in EDX:EAX
registers and are pushed with this code:
Assembler: push edx push eax |
Floating point values up to 32 bits are returned in the stack of the FPU. The generated code reserves space in the normal stack with a push command, and then stores the floating-point value.
Assembler: push eax fstp [esp] ; 32-bit store |
Floating point values from 33 up to 64 bits are processed in a similar way. The generated code is:
Assembler: push edx push eax fstp [esp] ; 64-bit store |
Note that the values pushed from EDX
and EAX
when the datun is floating point value are ignored and overwritten by the FSTP
instruction.
When a Logo function is made internal, this means it can be called from a function compiled in another language. The main issue is that the internal procedure expects parameters to be atoms, which the non-Logo function will pass processor's native data format.
Internalization of a Logo function builds a wrapper that converts native parameters to Logo atoms, then executes the Logo function and at the end converts its result back into native format.
The generated code for wrapper of internal functions is:
Logo: to func :a :b ... output :res end internal "func [i4 i4 i4] | Assembler: {prologue} ; standard prologue code push [ebp+{ofs}] ; push native param 2 call i4_to_atom ; convert native to atom push eax ; push atom push [ebp+{ofs}] ; push native param 1 call i4_to_atom ; convert native to atom push eax ; push atom mov [backup_frame],ebp ; store frame mov ebp,[root_frame] ; load frame of root mov esi,ebp ; set static link = dynamic link call {internal_func} mov ebp,[backup_frame] ; restore frame cmp [eax+{id}],error je $skip mov [error_flag],eax $skip: push eax ; push result atom call atom_to_i4 ; convert atom_to_native call deuse ; free param 1 call deuse ; free param 2 {epilogue} ; standard epilogue code ret |
The wrapper for internal functions sets explicitely the frame pointer before calling the internal function. This is needed to make sure that the internal function can access Logo variables. If the frame pointer is not set, then the execution will be confused, because it will not understand the stack structure of the calling function (the calling function is written in another language).