-
Notifications
You must be signed in to change notification settings - Fork 1
Proof-of-concept Lisp/Scheme interpreter
zephyrfalcon/dollop
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
# README # Crude notes about how this interpreter works. This interpreter uses a call stack that works as follows. Let's say we evaluate an expression E. If it's atomic, then we evaluate it and return its value. If it's composite (i.e. a list), then we evaluate all its elements, then treat it as a function call. Subexpressions that need evaluated go up the stack. E.g. (+ 1 2) First, we evaluate +. We put a placeholder in its place ($$) and push the subexpression + on the stack: ($$ 1 2) + Now we evaluate that subexpression. + is a symbol, which is looked up as a name, which refers to the built-in function <+>. We put that back and get the next element ready for evaluation: (<lambda:+> $$ 2) 1 (<lambda:+> 1 $$) 2 (<lambda:+> 1 2) Now we are done, and the whole expression can be evaluated. In this case, it's a call to a built-in function. The result is 3. => 3 If we apply a user-defined function, things look different. Let's say we have this function: (define twice (lambda (x) (* x 2))) First steps look the same: (twice 3) ($$ 3) twice (<lambda:twice> $$) 3 (<lambda:twice> 3) ;; ready to apply But when we apply the lambda, we do the following: 1. We create a new environment with as parent, the environment the lambda was defined in. 2. In that new environment, we bind parameters to the values passed. (In this case, x = 3) 3. We substitute the function call with the lambda body, associated with this new environment. (In other words, the lambda body (* x 2) will be evaluated in an environment that has x=3.) So: (* x 2) ($$ x 2) * (<lambda:*> $$ 2) x (<lambda:*> 3 $$) 2 (<lambda:*> 3 2) ;; ready to apply Here's another function application... but this one is built-in, so there are no more substitutions to be done: => 6 Special forms have different evaluation rules. Internally, the sf_next() function determines which elements have to be evaluated, and sf_apply() handles the actual application (e.g. define a variable, create a Lambda instance, etc). sf_apply() is also the place where tail recursion is handled. Currently only for BEGIN and IF; later, other special forms may follow. According to R*RS, the last expression inside a lambda body is also in tail position, but in our implementation lambda bodies can only contain one expression, so it doesn't apply here. (For multiple expressions use BEGIN, which *does* use TCO.) # # CONTINUATIONS Work like this: (+ 1 (call/cc (lambda (k) (+ 2 (k 3))))) ($$ 1 (call/cc ...)) + (<+> $$ (call/cc ...)) 1 (<+> 1 $$) (call/cc (lambda (k) (+ 2 (k 3)))) (<+> 1 $$) ($$ (lambda (k) (+ 2 (k 3)))) call/cc (<+> 1 $$) (<call/cc> $$) (lambda (k) (+ 2 (k 3))) (<+> 1 $$) (<call/cc> $$) <lambda> (<+> 1 $$) (<call/cc> <lambda>) At this point, we execute (<call/cc> <lambda>)... Now what? - We create a continuation (i.e. a snapshot of the current stack) - We create a function that takes an argument x, and that, when called, restores the call stack to the snapshot stored in the continuation, and then returns x - We replace the (<call/cc> <lambda>) with the lambda body, associated with an environment which has the lambda's variable bound to said function (<+> 1 $$) (+ 2 (k 3)) with k = {continuation} (<+> 1 $$) ($$ 2 (k 3)) + (<+> 1 $$) (<+> $$ (k 3)) 2 (<+> 1 $$) (<+> 2 $$) (k 3) (<+> 1 $$) (<+> 2 $$) ($$ 3) k (<+> 1 $$) (<+> 2 $$) ({cont} $$) 3 (<+> 1 $$) (<+> 2 $$) ({cont} 3) At this point, we're ready to call (k 3). As said, it restores the stack to the way it was, then collapses the value 3. So: (<+> 1 $$) 3 (<+> 1 3) => 4 The other, partially evaluated expressions, DO NOT MATTER as soon as we call (k 3).
About
Proof-of-concept Lisp/Scheme interpreter
Resources
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published