6 ms·
It's not just temerity I think. Picolisp is a very different animal compared to all modern lisp and scheme flavors. It is the last "true lisp" that I am aware o
by mppm 2y ago
It's not just temerity I think. Picolisp is a very different animal compared to all modern lisp and scheme flavors. It is the last "true lisp" that I am aware of -- it has an ultra-minimalist interpreter (hand-written in assembly, by the way) that actually represents programs as linked lists. A function is really just a list with the first element contains the argument and the second one the body, and the arguments are bare symbols. Picolisp has no compiler (not even a bytecode compiler), no lexical analysis and no other preprocessing. There is only the reader and the output goes directly to the interpreter. On the upside, this makes Picolisp the only language with truly "first class" functions in the sense that you can really create and manipulate them at runtime just like you would strings or integers, unlike pretty much every other language out there, where "lambdas" are just syntax sugar over function pointers. On the downside, this is all of course completely unchecked and pretty unsafe, and, to come back to the original point, you do not have such conveniences as lexical scoping. That would be literally impossible to implement without changing the nature of Picolisp into a proto-compiled language.
- lispm 2y ago> It is the last "true lisp" that I am aware of -- it has an ultra-minimalist interpreter (hand-written in assembly, by the way) that actually represents programs as linked lists. Strange, I thought many Lisp systems still have source level list-based interpreters. For Common Lisp I would think: SBCL (optional), CLISP, Allegro CL, LispWorks, ECL, ... They can also compile code. Compiling Lisp code was also already available in the first Lisp implementations and having a compiler was an explicit goal of the original implementors. Let's use the LispWorks Listener (the REPL tool): CL-USER 25 > (defun foo (bar) (break) (print (list :hello bar))) FOO CL-USER 26 > (foo 10) Break. 1 (continue) Return from break. 2 (abort) Return to top loop level 0. Type :b for backtrace or :c <option number> to proceed. Type :bug-form "<subject>" for a bug report template or :? for other options. CL-USER 27 : 1 > :bq INVOKE-DEBUGGER <- BREAK <- FOO <- EVAL <- CAPI::CAPI-TOP-LEVEL-FUNCTION <- CAPI::INTERACTIVE-PANE-TOP-LOOP <- MP::PROCESS-SG-FUNCTION CL-USER 28 : 1 > :n Call to INVOKE-DEBUGGER CL-USER 29 : 1 > :n Call to BREAK CL-USER 30 : 1 > :n Interpreted call to FOO CL-USER 31 : 1 > :lambda (LAMBDA (BAR) (DECLARE (SYSTEM::SOURCE-LEVEL #<EQ Hash Table{0} 81D03EFF03>)) (DECLARE (LAMBDA-NAME FOO)) (BREAK) (PRINT (LIST :HELLO BAR))) The debugger output of the currently running function looks like a linked list to me. I could modify it destructively, if I wanted. LispWorks has a list-level interpreter. One can also compile code. Typically Lisp interpreters tend to be written in C, since that usually is more portable than Assembler. > On the downside, this is all of course completely unchecked and pretty unsafe, and, to come back to the original point, you do not have such conveniences as lexical scoping. I would expect from a typical Lisp interpreter (a source level list-based interpreter) that it does all kinds of runtime checks and also provides lexical scoping. If there is a clojure, then this closure would be a combination of some function and an environment. In standard Common Lisp there is no access to that environment, but I could look from an inspector into it: CL-USER 54 > (defun example (a) (lambda () (break) (print a))) EXAMPLE CL-USER 55 > (example 10) #<anonymous interpreted function 8020001A59> CL-USER 56 > (describe *) #<anonymous interpreted function 8020001A59> is a TYPE::INTERPRETED-FUNCTION Code (LAMBDA NIL (BREAK) (PRINT A)) Environment ((A . 10) (#:SOURCE-LEVEL-ENVIRONMENT-MARKER FUNCTION NIL . #<EQ Hash Table{0} 81D03EFF03>) (#:FUNCTOR-MARKER LAMBDA (A) (DECLARE (SYSTEM::SOURCE-LEVEL #<EQ Hash Table{0} 81D03EFF03>)) (DECLARE (LAMBDA-NAME EXAMPLE)) (LAMBDA NIL (BREAK) (PRINT A)))) As we can see, the interpreted closure has code a list and an environment, where A = 10.
- mppm 2y ago> The debugger output of the currently running function looks like a linked list to me. I could modify it destructively, if I wanted. I doubt that actually, though I don't have LispWorks installed to try it. Modifying a function at runtime as if it were a list is actually the best test to see if your Lisp really represents functions as lists, or as some other internal object that is rendered as a list in the REPL by accessing the stored definition. E.g. both CLISP and guile error out if I try `(car (lambda (a b) (+ a b)))`. Another good test is to construct a function at runtime and try to call it. To do that you will probaby need to call `eval` or equivalent, just like you would in Lua or Python. Not in Picolisp though, which is why I consider it to be the only truly homoiconic programming language.
- lispm 2y ago> I doubt that actually You can doubt that. But I have done it. I know that it works. CL-USER 63 > (defun foo (a) (print 'hey) (print a)) FOO CL-USER 64 > (foo 'jack) HEY JACK JACK CL-USER 65 > (function-lambda-expression 'foo) (LAMBDA (A) (DECLARE (SYSTEM::SOURCE-LEVEL #<EQ Hash Table{0} 81D03EFF03>)) (DECLARE (LAMBDA-NAME FOO)) (PRINT (QUOTE HEY)) (PRINT A)) NIL FOO CL-USER 66 > (fifth *) (PRINT (QUOTE HEY)) CL-USER 67 > (setf (fifth (function-lambda-expression 'foo)) '(print 'hello)) (PRINT (QUOTE HELLO)) CL-USER 68 > (foo 'jack) HELLO JACK JACK > E.g. both CLISP and guile error out if I try `(car (lambda (a b) (+ a b)))`. Sure, but in CLISP the function is still a list internally. An interpreted function is a record, which stores the code as a list internally. The internally stored list is interpreted. Python compiles the code to byte code. CLISP has both a list-based interpreter and a byte code interpreter.
- mppm 2y agoMmh. Like I said, I'm not familiar with LispWorks, so take this with a grain of salt, but to me it looks like the system is just retrieving the original source expression that it keeps around in addition to the executable representation. But this is ultimately a question of implementation. My original point was that in Picolisp runtime-constructed lists are directly executable, without any processing. An unroller function that takes an action `foo` and a runtime number, e.g. 3, would return `'(() (foo) (foo) (foo))` and that would be it. In other Lisps you would first build the equivalent of this list and then pass it to `eval` to make it actually executable. Whether this step is expensive or not depends on the system. E.g. efficient closures require scanning the containing scopes and creating minimal state objects. Just storing the parent environment pointer would be super-inefficient and would prevent the entire environment from being garbage-collected, hence my claim that dynamic scope is the only thing that really makes sense in a direct-interpreted lisp, and that the presence of lexical scope implies some nontrivial processing before execution, though not necessarily as extensive as what would usually be called compilation. Edit: lexical analysis -> lexical scope