INTERPRETER INTERFACE Cute idea: (PROGV vars vals body) => (%PROGV vars vals #'(LAMBDA () body)). %PROGV is a totally magical function which establishes the bindings somehow, and then calls the body function. This would only be reasonable if we have stack closures. [### Actually, we should seriously consider flushing the interpreter, or at least 95% of it. There seem to be a variety of not-as-related-as-you-think reasons for having an interpreter: It is useful in initial system development to have a read-eval-print loop that doesn't require the compiler to work, but this doesn't require a full EVAL to be implemented. All you really need is calling of global macros and functions, self evaluating forms, accessing of global variables and functions (via FUNCTION), and the QUOTE and SETQ special forms. There is no need to support calling of interpreted functions, or the use of any other special forms. If the compiler is loaded when we encounter a hairy form, then we can compile the form and call the resulting function. MISC LOW-LEVEL STUFF Inline shallow binding: It's a bit tricky to correctly open-code special binding in the presence of interrupts. Here's way to do it: On binding, do the writes in this order: 1] Increment BS pointer. 2] Write old value on BS. 3] Write symbol on BS. 4] Write new value in symbol. On unbinding, in addition to restoring the old value, we also zero the slot on BS that held the symbol (before decrementing BS pointer.) When unbinding for a non-local exit (restore-dynamic-state), we ignore BS entries that have 0 in the symbol slot (and zero the symbol of ones that we undo.) Foreign function call: #| Idea: make our "miscop" calling convention be the same as the local C calling convention. This solves two problems at once by making call-miscop/miscop-in-lisp be the same as call-c/call-lisp-from-c. Then the Lisp global state registers should be in preserved registers? Otherwise, we would have to do some saving around true foreign calls. But getting C to save the Lisp global state doesn't work so good if we are going to support call-lisp-from-c, since the called routine is going to need to get at the current SP, BS, free-pointer, etc. But we still might want to put global state in preserved registers, since then we would only have to save, not save/restore. So the call-c/call-lisp-from-c case would be slightly different in that call-c would store global state at a known address and call-lisp-from-c would restore it, whereas no save/restore is required in the "miscop" case. But I guess the argument locations would also be different, since the C args are unboxed whereas the lisp "miscop" arguments would be boxed. So what would it really mean to say that the "miscop" convention is the same as C call? The set of preserved registers could be the same, which would make the VM definition less complex. (Though there would still be some asymmetry, since in a "miscop" case, the callee would save preserved boxed registers as boxed, whereas in call-lisp-from-c, all preserved registers would have to be immediately saved as unboxed and then cleared.) [Probably we need a global flag that is set when the registers are blessed as being OK for GC. This flag would be frobbed around call-to-c and in call-lisp-from-c and NLX entry.] But note that although call-to-c is complex in general, much of this complexity is due to a desire to support call-lisp-from-c. If we could somehow know that a C routine would never call out to Lisp, then we could dispense with saving the Lisp global state. But if signal handling is considered to be call-lisp-from-c, then this condition is pretty impossible to meet. But still, we might want to have some "miscop" VOPs that are actually implemented by calling a C procedure. Irrational functions, bcopy, etc. Also, I guess in call-to-c, we must jump indirectly through some static code so that we can GC without trashing C's raw return PC. The caller would set up the tagged return PC in a preserved register, then the "trampoline" would do a normal return to that PC when the C procedure returns. |# Note that after a foreign call, we have to clear all descriptor registers that might have been trashed. This is an incentive to use "preserved" (callee saves) registers for boxed registers. We don't have to worry about NLX trashing things: NLX already entry has to clear all boxed registers, since there might be raw vector pointers. It seems that all we really need to do for C callout is: -- Make a With-Stack-Alien frob that allocates an Alien on the C stack. -- Make a C-call special form (funny function) that takes the register arguments spread and the stack argument in a stack alien. The stack args will be all set up to do the call: all that is necessary is to move the register args and do the control transfer. -- May need some special coercion operations to convert to/from the representations used for register arguments and results [(unsigned-byte 32)?]. Convert a SAP to a C pointer, etc. -- Make the loader (and Genesis) handle load-time fixups for C calls, resulting in some sort of absolute branch immediate instruction. We need to allocate the arguments and result areas on the stack so that C callout routines will be reentrant. It is also virtually unavoidable to have Genesis do linking, since all syscalls and similar stuff will be done using C callout. Passing register arguments in registers is mainly an efficiency thing. Of course, setting up stack arguments where they are supposed to go and doing an immediate branch for the call are also major efficiency wins. Given these changes, calling C from Lisp shouldn't be much worse than calling C from C. There will be an extra lisp call on top unless the interface stub is proclaimed inline. With-Stack-Alien can just stash the C/number stack pointer and then decrement. Stack aliens will be allocated on top of any unboxed storage allocate by pack. The old SP is restored by an MV-Prog1 around the body. Non-local exits will restore the correct SP, since they just restore a saved value. Local exits from the body of With-Stack-Alien are forbidden. Obviously, any necessary return value Aliens should be allocated before the stack argument block so that the arguments will be on the stack top. The callout stub easily arranges for this by allocating the arguments with the innermost With-Stack-Alien. Assembly routines: [### How about this as a way of defining random assembler routines: (define-assembler-routines (( )*) ...assembler stuff...) The idea is that the body is evaluated a compile time, and the resulting code (contents of *code-vector*) is emitted as the definition. The Label-Var is bound to a label which should be emitted at the start of that routine. We allow multiple routines to be defined at once so that assembler routines can share code. Probably what we would do is segregate the assembler routines from normal code. We would then have an "assembler" that would open a fasl file and bind a few variables and then LOAD the "assembler source". The DEFINE-ASSEMBLER-ROUTINE macro would actually dump the code. Probably we can recycle the "assembler-code" and "assembler-labels" FOPs. At run-time this might as well just be a static I-vector, since nobody is ever going to point to the object header at runtime. All registers would have to be accessed using random TNs. I guess the macro could introduce lexical bindings of random TNs for all the registers so that don't have to make global variables for them. Maybe there should be a special WITH-RANDOM-TNS form that takes a list of the names of all the random TNs that the body needs. This might make it easier for people to keep track of which registers they use. The only thing that is really so weird as to possibly require special-case code is signal handling. But this is really just call-in from C (might need special support for signal handler functions that hacks interrupt stacks.) Note that signal handling doesn't need to be done until the system is basically up and running, so call-in won't require special genesis support or anything. Some mumblage about implementation issues for the RT implementation: Function call code sequences: @section(Returning from a Function Call) @label(Return) @index(Return) Returning is fairly simple, since we implement function call tail-recursively, guaranteeing that any stack values are left directly on top of the caller's stack frame. The caller is responsible for receiving or discarding the values. This isn't very onerous in the usual case of discarding any values, since this normally only requires moving CONT + a constant into CS. @subsection[Return Value Passing] Return values are passed in the same locations that arguments are, with the first three in registers and the rest on the top of the stack. However, the values count is handled somewhat differently than the argument count. There are two values passing conventions: single value and variable value. @subsection[Single Value Return Sequence] If we are returning exactly one value, then the sequence is simple. We put the value in A0 and return at 4 bytes after the return PC. Assuming the value is already in A0 and the return pc is the register SAVE-PC, the code is: @begin[example] inc SAVE-PC, 4 ; Mark as single-value. br SAVE-PC ; Go for it. @end[example] The increment of the PC could possibly be combined with any register-to-register move done on the PC. @subsection[Variable Value Return Sequence] If it is possible that we return other than one value, then we set any unused arg registers to NIL, place any extra values on the top of the stack, and return directly at the return PC. The variable value assuming the register values are already in place, and there are no stack values, the code sequence looks like this: @begin[example] ; Default unsupplied register values. lis NARGS, count ; Set number of values. br SAVE-PC ; Go. @end[example] If there are stack values, then there may be a miscop call that BLT's the values down on top of the current frame. Nothing clever, just Return-Values, taking Start, End and Count arguments. This miscop will only be needed when the compiler didn't initially allocate the values at the beginning of the frame, which is usually possible when the number of values being returned is known. @subsection[Single Value Entry Sequence] The return point for receiving a single value is fairly simple, since all it needs to do is blow away any stack values that might be there: @begin[example] cal CS, CONT, size ; Restore CS. lm CONT, start-reg, -nregs ; Restore registers. @end[example] This sequence must immediately follow the balr instruction, since the callee adds to the return PC to get the correct entry point. Note that in the single-value return case, we don't execute the instruction, although it would be harmless to do so. @subsection[Multiple Value Entry Sequence] The return point for receiving two or three values is also simple. If variable values are returned, we discard any stack values. If a single value is returned, we default the unsupplied values: @begin[example] b mv-tag ; If MV's, skip. cau A1, nil-h ; Default unsupplied values. b done mv-tag: cal CS, CONT, size ; Flush stack values. done: lm CONT, start-reg, -nregs ; Restore registers. @end[example] If there are stack values, then we emit code to NIL out the unsupplied values using a miscop call: @begin[example] b mv-tag ; If MV's, skip. cau A1, nil-h ; Default register values. b done lis NARGS, 3 ; Set value count. mv-tag: cal CS, CONT, size ; Set CS for desired values. dec NARGS, nvals ; Number to default. miscop default-values ; Default them. lm CONT, start-reg, -nregs ; Restore registers. @end[example] Default-Values just sets to NIL the -NARGS cells under CS.