# Some sort of a Lisp *I don't know Lisp, but I'm going to write one anyways* The unit at comp/lisp.fs implements some sort of a Lisp with good interoperability with the Dusk system. It doesn't adhere to other Lisp standards, but was developed while mainly looking at Common Lisp. As with [comp/c], the broad idea is that Lisp functions and Forth words are pretty much the same, except that Lisp, like C, can only handle "single result signatures", that is ( ... -- res ). ## Invocation You can interpret a Lisp expression with "lisp ( -- res )" followed by an opening "(" followed by your expression. When the closing parens is encountered, the result, which can be either a straight number or a cons [mem/cons]. For example: needs comp/lisp lisp (- 42 (+ 2 3)) . will print 37. You can also use "lisp. ( -- )" which will pretty-print the result instead using .cons [mem/cons]. You can also interpret whole files with "lisp<<". This will repeatedly interpret all parens in the file until the end of file is reached, dropping the result at each iteration. Example: lisp<< foo.l ## Language The language implemented by this unit looks like other lisps. In general, parens are a function invocation, with the first element being a reference to another lisp function or a system word. Then, arguments follow. Lisps functions and Forth words follow the same call semantics. For example, when "(+ 2 3)" is executed, 2 and then 3 are placed on PS and "+", the same "+" as in Forth, is called, yielding the result on PS. All Lisp functions yield exactly one result to PS. Parens can be nested. Nested parens are executed first, their result being placed on PS as arguments for the outer parens. ## Quoting Parens can mean plain data if they are quoted with the "'" character. For example, "'(+ 2 3)" doesn't actually call "+", but creates a list (in [mem/cons]) with the address of "+" followed by 2 and 3 in a list and yields a pointer to that list. ## Comments Comments use the "\" token and behaves like the system "\", that is, it drops input stream until the end of the line. For example: lisp (+ \This is a comment 2 3) will yield 5. ## String literals String literals can be used pretty much like in Forth. They yield pointers to their counted string. For example, "lisp (c@ "hello")" will yield 5. ## Compiling "lambda" and "defun" execute their body in a particular manner: they compile the native code corresponding to it using the HAL [doc/hal]. For example: lisp (defun foo (a b) (+ a b)) is the rough equivalent to: code foo PSP) +, nip, exit, (but less efficient because PS preservation logic forces us to put some fluff around that) ## Calling Forth words TODO: this unit was written before [comp/sig] existed. Ideally, comp/lisp should be changed to use [comp/sig] like other compilers in Dusk do. As mentioned above, this lisp can freely call any Forth word from the system dictionary: : foo ( a b -- res ) 2* + ; lisp. (foo 42 54) the lisp invocation is the equivalent to "42 54 foo". When calling Forth words, no signature check is made. If you make a call with the wrong number of arguments, you will corrupt PS and hell will ensue. Maybe you can live with that, but there's a deeper problem: you cannot call a word that has no return value, for example stype ( str -- ). This automatically results in PS corruption. But there's a solution! You can register signatures with "addsig": (addsig (noret 1 'stype) (stype "This doesn't corrupt PS!") "sig" takes an argument count first, then a "no return" flag to indicate that the target word has no return value. When it happens, a dummy 0 is added to PS after the call, keeping PS balance. When a word has a signature, we check that the number of arguments we're passing corresponds to the number of arguments in the signature, aborting if it doesn't. "defun" and "lambda" automatically register signatures. ## Parsers There are exceptions to the regular parsing logic that we call "parsers", for example "lambda". These words have a signature that is incompatible with the regular resolving logic. For example, in "(defun foo ...)", if we let the default resolver pick it up, we'll look for "foo" in the system dict and not find it, which is normal because it's the name of the word we want to create! So when we encounter these parser words, we circumvent the regular resolving mechanism and call the parser word while supplying it with the raw list directly. We then expect that parser to yield a result that we can feed into our cooking process. The list of parser words is: lambda defun let to ## Forth API lisp ( -- res ) Parse a single expression from the input stream and execute it, yielding its result. lisp. ( -- ) Same as "lisp", but prints the result with ".cons" [mem/cons]. lisp<< ( "path" -- ) Opens file "path" and interprets all parens in the file, dropping all results. ## Lisp API addsig ( sig w ) Binds signature "sig" to word address "w". "sig" is mostly an argument count, but can have bits flipped with the functions described below. Example: (addsig (noret 1) 'stype) noret ( sig ) compiler ( sig ) parser ( sig ) Sets the "no return/compiler/parser" bit on "sig". apply ( w list ) Unroll "list" to PS and execute "w". For example, "(apply '+ '(2 3))" yields 5. equal ( a b ) Generally yields the same as "=", but if "a" <> "b" and that both are cons, recursively compare cons car and cdr. If they're equal, yield 1. eval ( list ) Unroll "list" to PS and execute its first element. For example, "(eval '(+ 2 3))" yields 5. length ( list ) Return the number of elements in "list". map ( w list ) Execute "w", which has an expected signature of (x -- x), to every elements of "list". Results yielded are used to create a new cons, which is yielded. For example, "(map '1+ '(1 2 3))" yields (2 3 4). to ( target expr ) Given that "target" resolves to a setter [doc/usage/word] resolve "expr" and set "target" to its value using regular "to" semantics. Yield the value of "expr". For example, given that we have a value "foo", "(to foo 42)" is equivalent to "42 to foo" in Forth. ## Compiler API Except for "lambda" and "defun", words in this section are only valid inside "lambda" or "defun". lambda ( (args) (exprs) ) Parse a function definition from input stream and compile it. The syntax for such a definition is "(args) [(body)]". Example: (lambda (a b) (- b a)) yields word "w" (which can be called like any Forth word), which does the equivalent of "swap -". defun ( name (args) (exprs) ) Same as "lambda", but creates a dictionary entry for the created word. Syntax is "name (args) [(body)]". let ( (initexprs) (exprs) ) This construct creates new PS slots, binding them to name and filling them with the result of their initialization expression. Then, using those bindings, execute "exprs", yielding a single result. For example: lisp. (defun foo (a) (let ((b (+ 2 42))) (- b a))) 3 foo . \ prints 41 if ( cond true false ) Compile "cond", when compile a conditional branch to either "true" or "false", which are also compiled in a regular manner, depending on whether "cond" yielded zero (we branch to "false") or nonzero (we branch to "true"). tailcall ( ... ) Make a jump to the beginning of the function currently being compiled, allowing recursion without growing RS. This of course can only work as expected when is done at the very end of the body. Example: (defun fib (n) ((lambda (n a b) (if n (tailcall (- n 1) b (+ a b)) a)) n 0 1))