Lisp has no compile time vs run time distinction. It only has lists. What is done with a list depends on what the first thing in the list is determined to be:
1. Function - evaluate the arguments then pass it to the function.
2. Macro - manipulate the list according to the macro and then get another list.
3. Special Form - follow the special rules for the special form. (The list of special forms varies from Lisp to Lisp, and are the elementary building blocks from which the language was built.)
In Lisp, macros are advertised as code operating on itself, because that is exactly what they are. And yes, they are exactly that cool.
That is from one of the founders of YCombinator, the author of http://www.paulgraham.com/onlisptext.html and http://www.paulgraham.com/acl.html.
It is certainly true that macros are a different kind of code from most ordinary code, because what they're doing is different. But importantly, they are written in the same Turing-complete language. I once wrote a program (a C implementation for Lisp Machines) that allowed the user to interactively execute C expressions. Obviously, it had to parse the C code according to the syntax of that language, but the parser produced list structure that consisted entirely of macro calls. The guts of the compiler were implemented as a collection of macros that translated the output of the parser into Lisp Machine Lisp. These macros did things like accessing and updating a symbol table — far beyond what you could do in a C macro.
[0] https://www.ccs.neu.edu/home/stchang/pubs/ckg-popl2017.pdf
There are Lisps that improve on the situation. In particular, vau calculi/fexprs are worth examining. http://lambda-the-ultimate.org/node/4093
There are also systems like Forths, where the compiler and interpreter really are the same system, in two different modes of operation. In these systems, there is only runtime; compiletime is a specific style of runtime.
The main contrast that I would draw between Forths and Lisps is syntax. Forths don't really have syntax; they have token-parsing streams. Lisps are extremely tree-oriented, but Forths are stack-oriented.
In a compiled implementation, code needs to be compiled before it gets run. When I want it to be compiled, I have to call functions like COMPILE or COMPILE-FILE.
ti.arc.nasa.gov/m/pub-archive/176h/0176%20(Havelund).pdf
For compiled code, there usually will be no macro expansion at runtime. Lisp will not macro expand code in compiled code, since all macros need to be expandable at compile-time.
But: if you explicitly want it, you can generate code at runtime and then you need to call EVAL or COMPILE. Then macro expansions might happen.
"Most Common Lisp implementations support creating native-format executable files. The typical route involves loading all of the program's code into the Lisp environment, dumping the current state of the Lisp process into a disk image, and identifying a start function (ie main).
Since the disk image contains the entire Common Lisp system (compiler, debugger, documentation strings, etc.), the executable will be typically larger than a compiled C program (even if you statically link the C code).
Another form of disk image requires a copy of the particular Lisp implementation to be installed on the recipient's machine. This is no different in principle from a C application requiring platform-specific dynamic libraries to run. This complicates deployment - the disk image is tied to a particular implementation version, and you typically end up having to include the Lisp implementation executable anyway.
Due to platform limitations, some Lisp implementations can't produce native executables (for example, ABCL can only make JAR files). Other implementations are only able to dump disk images that require a separate copy of the implementation executable. An alternative to executables is available on Unix platforms in the form of shell scripting."
If it needs 4 wordy paragraphs and you still don't know how to do it, I am going to scribe that up as non-trivial.
I have this alias in my .zshrc:
clc() { sbcl --no-userinit --load $1 --eval "(sb-ext:save-lisp-and-die \"$(sansext $1)\" :toplevel 'main :executable t)" }
Sansext is a custom function that strips the file extension from a filename, but you don't need that; you can read output filename from $2, or whatever else.For SBCL one needs to call a function, which creates the executable image.
http://www.sbcl.org/manual/index.html#Function-sb_002dext-sa...
There are more complex machineries for application generation, though.
This is one of the big advantages of Lisp macros over C macros. In Lisp you write macros using Lisp, including normal user-defined functions. In C, on the other hand, you write macros using "C Preprocessor directives"
https://docs.racket-lang.org/guide/stx-phases.html
I understand there may be some philosophical or theoretical interest in framing Lisp the way you do, but does that invalidate the close-to-the-app thinking about the costs of macros and where those costs are distributed?
In SBCL, we would commonly say that code is "evaluated" rather than compiled or interpreted. This is because "compiler" is referring specifically to when assembly is emitted and "interpreter" is referring to when code is being used to call already compiled code. In the case of SBCL, it is doing both of these things simultaneously when it is reading a ".lisp" file. For example, if this is the contents of my .lisp file:
(defun boop () (print "hello"))
(boop) ;; prints hello
(defun scoop () (print "goodbye"))
The boop function is being evaluated (and therefore compiled), and then called on the next line. In traditional algol based languages like c and java with a separate compilation phase, it would not be possible to call boop before scoop has been turned into machine code.
So you are asking how the costs of evaluating lisp are distributed?
For one, the assembler generated whilst evaluating the .lisp file can be cached in a .fasl file so that it does not need to be evaluated again to be loaded into other projects.
Another is that macro's can be evaluated once during function definition. e.g.
(defmacro moob () `(print "kellog"))
(defun foo () (moob)) ; evaluates to (defun funky () (print "kellog"))
(defmacro moob () `(print "fish")) ; redefining moob
(foo) ; will print "kellog" because funky was evaluated before moob was redefined.
In this case, the cost of calling moob is trivial because it only occurs once during the definition of foo. It will slow down the initial load of your program, but no subsequent calls to foo.
SBCL has both an in-core compiler (compiles to machine code in memory) and a file compiler (compiles to machine code in an file).
SBCL also has an interpreter (relatively new and which usually is not used), which executes Lisp source as data.
When running in the REPL (the most common way to interface with Lisp) the compile time and runtime Lisps are the same.
For example, here's a copy/paste from a REPL session that defines a macro that defines a function. The macro (at "compile time") prints information about the function (just its argument count) to stdout before using defun (itself a macro) to actually define the function.
Next I call the new function and print out a disassembly, just to show the function is in fact compiled
"CL-USER> " is the prompt in the REPL I use:
CL-USER> (defmacro my-defun (name arguments &body body)
(format t "Defining ~a, taking ~a arguments~%" name (length arguments))
`(defun ,name ,arguments ,@body))
MY-DEFUN
CL-USER> (my-defun omg-2 (value) (* value value))
Defining OMG-2, taking 1 arguments
OMG-2
CL-USER> (omg-2 34)
1156
CL-USER> (disassemble #'omg-2)
; disassembly for OMG-2
; Size: 33 bytes. Origin: #x52ED2714 ; OMG-2
; 14: 498B5D10 MOV RBX, [R13+16] ; thread.binding-stack-pointer
; 18: 48895DF8 MOV [RBP-8], RBX
; 1C: 488BD6 MOV RDX, RSI
; 1F: 488BFE MOV RDI, RSI
; 22: FF1425C0001052 CALL QWORD PTR [#x521000C0] ; GENERIC-*
; 29: 488B75F0 MOV RSI, [RBP-16]
; 2D: 488BE5 MOV RSP, RBP
; 30: F8 CLC
; 31: 5D POP RBP
; 32: C3 RET
; 33: CC10 INT3 16 ; Invalid argument count trap
NIL
CL-USER>
It is also possible to compile a Lisp program to an executable (or byte code or whatever) and run it, and not use the dynamic aspect of it.In an interpreter version of Lisp, the interpreter may expand the macros at runtime.
What you do is, you use macroexpand-1, like so:
https://stackoverflow.com/questions/50754347/macro-with-a-li...