44
\$\begingroup\$

Lisp programmers boast that Lisp is a powerful language which can be built up from a very small set of primitive operations. Let's put that idea into practice by golfing an interpreter for a dialect called tinylisp.

Language specification

In this specification, any condition whose result is described as "undefined" may do anything in your interpreter: crash, fail silently, produce random gobbldegook, or work as expected. A reference implementation in Python 3 is available here.

Syntax

Tokens in tinylisp are (, ), or any string of one or more printable ASCII characters except parentheses or space. (I.e. the following regex: [()]|[^() ]+.) Any token that consists entirely of digits is an integer literal. (Leading zeros are okay.) Any token that contains non-digits is a symbol, even numeric-looking examples like 123abc, 3.14, and -10. All whitespace (including, at a minimum, ASCII characters 32 and 10) is ignored, except insofar as it separates tokens.

A tinylisp program consists of a series of expressions. Each expression is either an integer, a symbol, or an s-expression (list). Lists consist of zero or more expressions wrapped in parentheses. No separator is used between items. Here are examples of expressions:

4
tinylisp!!
()
(c b a)
(q ((1 2)(3 4)))

Expressions that are not well-formed (in particular, that have unmatched parentheses) give undefined behavior. (The reference implementation auto-closes open parens and stops parsing on unmatched close parens.)

Data types

The data types of tinylisp are integers, symbols, and lists. Built-in functions and macros can also be considered a type, though their output format is undefined. A list can contain any number of values of any type and can be nested arbitrarily deeply. Integers must be supported at least from -2^31 to 2^31-1.

The empty list ()--also referred to as nil--and the integer 0 are the only values that are considered logically false; all other integers, nonempty lists, builtins, and all symbols are logically true.

Evaluation

Expressions in a program are evaluated in order and the results of each sent to stdout (more on output formatting later).

  • An integer literal evaluates to itself.
  • The empty list () evaluates to itself.
  • A list of one or more items evaluates its first item and treats it as a function or macro, calling it with the remaining items as arguments. If the item is not a function/macro, the behavior is undefined.
  • A symbol evaluates as a name, giving the value bound to that name in the current function. If the name is not defined in the current function, it evaluates to the value bound to it at global scope. If the name is not defined at current or global scope, the result is undefined (reference implementation gives an error message and returns nil).

Built-in functions and macros

There are seven built-in functions in tinylisp. A function evaluates each of its arguments before applying some operation to them and returning the result.

  • c - cons[truct list]. Takes two arguments, a value and a list, and returns a new list obtained by adding the value at the front of the list.
  • h - head (car, in Lisp terminology). Takes a list and returns the first item in it, or nil if given nil.
  • t - tail (cdr, in Lisp terminology). Takes a list and returns a new list containing all but the first item, or nil if given nil.
  • s - subtract. Takes two integers and returns the first minus the second.
  • l - less than. Takes two integers; returns 1 if the first is less than the second, 0 otherwise.
  • e - equal. Takes two values of the same type (both integers, both lists, or both symbols); returns 1 if the two are equal (or identical in every element), 0 otherwise. Testing builtins for equality is undefined (reference implementation works as expected).
  • v - eval. Takes one list, integer, or symbol, representing an expression, and evaluates it. E.g. doing (v (q (c a b))) is the same as doing (c a b); (v 1) gives 1.

"Value" here includes any list, integer, symbol, or builtin, unless otherwise specified. If a function is listed as taking specific types, passing it different types is undefined behavior, as is passing the wrong number of arguments (reference implementation generally crashes).

There are three built-in macros in tinylisp. A macro, unlike a function, does not evaluate its arguments before applying operations to them.

  • q - quote. Takes one expression and returns it unevaluated. E.g., evaluating (1 2 3) gives an error because it tries to call 1 as a function or macro, but (q (1 2 3)) returns the list (1 2 3). Evaluating a gives the value bound to the name a, but (q a) gives the name itself.
  • i - if. Takes three expressions: a condition, an iftrue expression, and an iffalse expression. Evaluates the condition first. If the result is falsy (0 or nil), evaluates and returns the iffalse expression. Otherwise, evaluates and returns the iftrue expression. Note that the expression that is not returned is never evaluated.
  • d - def. Takes a symbol and an expression. Evaluates the expression and binds it to the given symbol treated as a name at global scope, then returns the symbol. Trying to redefine a name should fail (silently, with a message, or by crashing; the reference implementation displays an error message). Note: it is not necessary to quote the name before passing it to d, though it is necessary to quote the expression if it's a list or symbol you don't want evaluated: e.g., (d x (q (1 2 3))).

Passing the wrong number of arguments to a macro is undefined behavior (reference implementation crashes). Passing something that's not a symbol as the first argument of d is undefined behavior (reference implementation doesn't give an error, but the value cannot be referenced subsequently).

User-defined functions and macros

Starting from these ten built-ins, the language can be extended by constructing new functions and macros. These have no dedicated data type; they are simply lists with a certain structure:

  • A function is a list of two items. The first is either a list of one or more parameter names, or a single name which will receive a list of any arguments passed to the function (thus allowing for variable-arity functions). The second is an expression which is the function body.
  • A macro is the same as a function, except that it contains nil before the parameter name(s), thus making it a three-item list. (Trying to call three-item lists that do not start with nil is undefined behavior; the reference implementation ignores the first argument and treats them as macros as well.)

For example, the following expression is a function that adds two integers:

(q               List must be quoted to prevent evaluation
 (
  (x y)          Parameter names
  (s x (s 0 y))  Expression (in infix, x - (0 - y))
 )   
)

And a macro that takes any number of arguments and evaluates and returns the first one:

(q
 (
  ()
  args
  (v (h args))
 )
)

Functions and macros can be called directly, bound to names using d, and passed to other functions or macros.

Since function bodies are not executed at definition time, recursive functions are easily definable:

(d len
 (q (
  (list)
  (i list                      If list is nonempty
   (s 1 (s 0 (len (t list))))  1 - (0 - len(tail(list)))
   0                           else 0
  )
 ))
)

Note, though, that the above is not a good way to define a length function because it doesn't use...

Tail-call recursion

Tail-call recursion is an important concept in Lisp. It implements certain kinds of recursion as loops, thus keeping the call stack small. Your tinylisp interpreter must implement proper tail-call recursion!

  • If the return expression of a user-defined function or macro is a call to another user-defined function or macro, your interpreter must not use recursion to evaluate that call. Instead, it must replace the current function and arguments with the new function and arguments and loop until the chain of calls is resolved.
  • If the return expression of a user-defined function or macro is a call to i, do not immediately evaluate the branch that is selected. Instead, check whether it is a call to another user-defined function or macro. If so, swap out the function and arguments as above. This applies to arbitrarily deeply nested occurrences of i.

Tail recursion must work both for direct recursion (a function calls itself) and indirect recursion (function a calls function b which calls [etc] which calls function a).

A tail-recursive length function (with a helper function len*):

(d len*
 (q (
  (list accum)
  (i list
   (len*
    (t list)
    (s 1 (s 0 accum))
   )
   accum
  )
 ))
)
(d len
 (q (
  (list)
  (len* list 0)
 ))
)

This implementation works for arbitrarily large lists, limited only by the max integer size.

Scope

Function parameters are local variables (actually constants, since they can't be modified). They are in scope while the body of that call of that function is being executed, and out of scope during any deeper calls and after the function returns. They can "shadow" globally defined names, thereby making the global name temporarily unavailable. For example, the following code returns 5, not 41:

(d x 42)
(d f
 (q (
  (x)
  (s x 1)
 ))
)
(f 6)

However, the following code returns 41, because x at call level 1 is not accessible from call level 2:

(d x 42)
(d f
 (q (
  (x)
  (g 15)
 ))
)
(d g
 (q (
  (y)
  (s x 1)
 ))
)
(f 6)

The only names in scope at any given time are 1) the local names of the currently executing function, if any, and 2) global names.

Submission requirements

Input and output

Updated June 2022 to be more flexible

Your interpreter should input a tinylisp program in one of two ways:

  • directly, as a multiline string, or
  • indirectly, as the name of a file containing the tinylisp program.

It should evaluate each expression in the program one by one, outputting each result with a trailing newline.

  • Integers should be output in your implementation language's most natural representation. Negative integers can be output, with leading minus signs.
  • Symbols should be output as strings, with no surrounding quotes or escapes.
  • Lists should be output with all items space-separated and wrapped in parentheses. A space inside the parentheses is optional: (1 2 3) and ( 1 2 3 ) are both acceptable formats.
  • Outputting built-in functions and macros is undefined behavior. (The reference interpretation displays them as <built-in function>.)

All default input and output methods are acceptable. Your submission may be a full program or a function.

Other

The reference interpreter includes a REPL environment and the ability to load tinylisp modules from other files; these are provided for convenience and are not required for this challenge.

Test cases

The test cases are separated into several groups so that you can test simpler ones before working up to more-complex ones. However, they will also work just fine if you dump them all in one file together. Just don't forget to remove the headings and the expected output before running it.

If you have properly implemented tail-call recursion, the final (multi-part) test case will return without causing a stack overflow. The reference implementation computes it in about six seconds on my laptop.

\$\endgroup\$
24
  • \$\begingroup\$ "Any token that consists entirely of digits is an integer literal. (Leading zeros are okay.) Any token that contains non-digits is a symbol, even numeric-looking examples like 123abc, 3.14, and -10." seems to contradict "Integers must be supported at least from -2^31 to 2^31-1." \$\endgroup\$
    – msh210
    Commented Nov 4, 2015 at 6:24
  • 3
    \$\begingroup\$ @msh210 Not really, because the former is talking about tokens whereas the latter is talking about values. Even though there's no direct way to enter -1, I can still generate the value -1 by doing (s 0 1). \$\endgroup\$
    – DLosc
    Commented Nov 4, 2015 at 6:51
  • 1
    \$\begingroup\$ @coredump After reading the pertinent Wikipedia article, I've concluded that the implementation is actually closer to dynamic, but with no scope nesting. Variables in function F are not available in function G if F calls G (as with dynamic scoping), but they are also not available in function H if H is a nested function defined inside F (as with lexical scoping)--see test case 5. So calling it "lexical" might be misleading. \$\endgroup\$
    – DLosc
    Commented Nov 4, 2015 at 18:37
  • 2
    \$\begingroup\$ To put it another way: because of the lack of scope nesting, an implementation could use either a dynamic or a lexical scoping strategy and come up with the same results. The only names in scope at any given time are 1) the local names of the currently executing function, if any, and 2) global names. Closures are not supported. (The reference implementation keeps a stack of name bindings corresponding to the call stack--a dynamic-style approach, which I think will be the easiest to implement.) \$\endgroup\$
    – DLosc
    Commented Nov 4, 2015 at 18:54
  • 2
    \$\begingroup\$ Obligatory xkcd. \$\endgroup\$
    – user42643
    Commented Nov 5, 2015 at 10:36

7 Answers 7

23
+1200
\$\begingroup\$

Acc!!, 9450 9334 bytes

Count h while h-10 {
_+(258+h*2^8+2^73+(882+h*24)*(2^80+0^0^(9-h)*(4^56+2^40))+(15*4^56+6*2^40)*0^0^(9-h)+257*4^72+(684743974900/20^h%20+99)*4^92)*256^(864+h*24)
}
_+1113*2^352+2^8833+69*2^292+2^262+69*2^516
Count h while _/2^320%2^32+0^h {
_+(N-_/2^320%2^8)*2^320
_+(1-3302829851648/2^(_/2^320%2^32)%2+2^32*0^(_/2^448%2^32)-_/4^64%2^64)*4^64
Count f while 0^(_/4^64%2^64)+0^(_/2^320%2^32-40)^2-f {
_+(1-_/2^64%2^64)*2^64
Count g while _/2^64%2^32*(_/2^448%2^32)*(g-_/256^(_/2^448%2^32+1)%2^32) {
_+(_/256^(_/2^448%2^32+g+5)%2^8-_/2^32%2^32)*2^32
_+(1023*2^48/2^(_/2^32%2^32)%2+2^32*(_/2^96%2^32*10+_/2^32%2^32-48)-_/2^64%2^64)*2^64
}
_+(_/2^96%2^32*2^8*256^(_/2^352%2^32)+(_/2^352%2^32-_/2^448%2^32)*2^448+5*2^352)*0^0^(_/2^64%2^32*(_/2^448%2^32))
_+(2+_/2^448%2^32*2^8)*2^(_/2^352%2^32*8)+(_/256^(_/2^256%2^32)%2^32-_/2^32%2^32)*2^32
_+(_/2^352%2^32-_/2^(_/2^256%2^32*8)%2^64)*2^(_/2^256%2^32*8)+_/2^352%2^32*256^((_/256^(_/2^256%2^32-4)%2^32-4)*0^(_/2^32%2^32)+_/2^32%2^32+5)+9*2^352
_+0^(_/2^448%2^32)*8^86-_/2^448%2^32*2^448
}
_+(256^(_/2^352%2^32)+(_/2^352%2^32-_/2^448%2^32)*2^448+5*2^352)*(_/4^80%2^32)*(_/4^64%2^32)
_+(_/2^320%2^32*256^(_/2^352%2^32)+2^352+256^(_/2^448%2^32+1))*(_/4^64%2^32)
_-0^(_/2^320%2^32-41)^2*(_/256^(_/2^256%2^32)%2^32*256^(_/2^256%2^32)+8^86)
}
Count d while _/256^(_/8^96%2^32+5)%2^32 {
_+(_/256^(_/8^96%2^32+5)%2^32-_/8^96%2^32)*8^96
_+(64-_/2^224%2^32)*2^224
_+(2+2^8*11+2^73+2^80*(_/256^(_/8^96%2^32+1)%2^32)-_/2^(_/2^352%2^32*8)%4^72)*2^(_/2^352%2^32*8)+((_/2^352%2^32+9)*(2^32+1)-9-_/2^(_/2^224%2^32*8)%2^64)*2^(_/2^224%2^32*8)+9*2^353
Count f while _/2^224%2^32/64 {
_+(_/2^(_/2^224%2^32*8)%2^64-_/4^80%2^64)*4^80
_+(_/256^(_/4^80%2^32+1)%2^64*0^0^(_/4^80%2^32)-_/2^32%2^64)*2^32
_+(((_/256^(_/2^32%2^32+5)%2^32-_/2^32%2^32)*0^0^(_/2^32%2^32/64)+2^32*(_/4^96%2^32))*256^(_/4^80%2^32+1)-_/256^(_/2^224%2^32+4)%2^32*256^(_/2^224%2^32+4))*0^0^(_/2^32%2^32*0^(_/2^64%2^32)*(0^(_/2^32%2^32-8)^2+0^(_/256^(_/2^32%2^32+1)%2^32)*(_/2^32%2^32/64)))+(_/4^96%2^32*256^(_/2^64%2^32+5)-_/256^(_/2^224%2^32+4)%2^32*256^(_/2^224%2^32+4))*0^(0^(_/2^64%2^32)+0^0^(_/2^64%2^32)*(_/256^(_/2^64%2^32+5)%2^32)+(_/2^32%2^32-9)^2)+(_/4^96%2^32*256^(_/4^80%2^32+5)+(_/256^(_/4^96%2^32+5)%2^32-_/256^(_/2^224%2^32+4)%2^32)*256^(_/2^224%2^32+4)-_/256^(_/4^96%2^32+5)%2^32*256^(_/4^96%2^32+5))*0^(_/2^32%2^32-10)^2*0^(_/2^64%2^32)
_+(_/2^(_/2^224%2^32*8)%2^64-_/4^80%2^64)*4^80
Count n while _/4^96%2^32*0^n {
_+(_/256^(_/4^96%2^32+1)%2^32-_/2^416%2^32)*2^416
Count p while 0^(1-_/256^(_/2^416%2^32)%2^8)^2*0^p {
Count e while (_/2^224%2^32-8*e)/72*0^(_/2^384%2^32)+0^e {
_+(_/256^(_/2^224%2^32-8*e-8)%2^64-_/2^96%2^64)*2^96
_+(_/2^96%2^32*0^0^(_/256^(_/2^96%2^32+1)%2^32/64*0^(_/4^64%2^32))-_/2^384%2^32)*2^384
}
_+(_/256^(_/256^(_/2^384%2^32+1)%2^32+1)%2^32+2^32*(_/256^(_/2^384%2^32+5)%2^32)-_/2^32%2^64)*2^32
_+((2+2^8*(_/2^32%2^32)+2^73+2^80*(_/2^64%2^32))*256^(_/2^352%2^32)+((_/2^352%2^32+9)*(2^32+1)-9-_/2^32%2^64)*2^32+9*2^353)*0^0^(2-_/2^(_/2^32%2^32*8)%2^8)
Count e while 0^(_/2^480%2^32)*(e-2)+0^e {
_+(873+2^32*864-_/2^32%2^64)*2^32*e
Count g while _/2^32%2^32 {
_+(_/256^(_/2^32%2^32+5)%2^32+2^32*(_/256^(_/2^64%2^32+5)%2^32)-_/2^32%2^64)*2^32*0^0^g
_-_/2^32%2^32*2^32*0^((_/256^(_/2^416%2^32+1)-_/256^(_/256^(_/2^32%2^32+1)%2^32+1))%256^(_/256^(_/2^416%2^32+1)%2^32+4))
}
_+(_/256^(_/2^64%2^32+1)%2^32-_/2^480%2^32)*2^480
}
}
_+(_/2^416%2^32-_/2^480%2^32)*0^(_/256^(_/2^416%2^32)%2^8)*2^480
Count p while (2-_/2^(_/2^416%2^32*8)%2^8)*0^p {
Count h while 0^0^(_/2^320%2^32)*(_/256^(_/2^320%2^32+5)%2^32)+0^h {
_+(_/256^(_/2^320%2^32+5)%2^32*0^0^h+_/4^80%2^32*0^h-_/2^320%2^32)*2^320
}
_+(2+_/2^480%2^32*2^8)*2^(_/2^352%2^32*8)+_/2^352%2^32*256^(_/2^320%2^32+5+(_/2^224%2^32-5)*0^(_/2^320%2^32))+9*2^352
_+(_/256^(_/4^96%2^32+5)%2^32-_/256^(_/2^224%2^32+4)%2^32)*256^(_/2^224%2^32+4)
}
_+(2^227+_/2^416%2^32*2^96*256^(_/2^224%2^32))*0^(2-_/2^(_/2^416%2^32*8)%2^8)
}
_+(_/2^(_/2^224%2^32*8)%2^64-_/2^32%2^64)*2^32
_+(_/256^(_/2^32%2^32+1)%2^64-_/2^96%2^64)*2^96
Count c while 0^(_/2^64%2^32+c) {
_+(_/256^(_/4^64%2^32+1)%2^32+2^32*(_/256^(_/256^(_/4^64%2^32+5)%2^32+1)%2^32)-_/4^80%2^64)*4^80
Count p while 0^(_/2^96%2^32-11)^2*0^p {
Write 40)*(0^(2-_/2^(_/4^80%2^32*8)%2^8)+0^(_/4^80%2^32)
_+(_/4^80%2^32-_/2^(_/2^352%2^32*8)%2^32)*2^(_/2^352%2^32*8)
_+(_/2^352%2^32+4-_/2^256%2^32)*2^256
Count h while _/2^256%2^32-_/2^352%2^32 {
_+(_/2^((_/2^256%2^32-4)*8)%2^32-_/2^320%2^32)*2^320
Count l while (2-_/2^(_/2^320%2^32*8)%2^8)*0^l {
Write 41)*0^(_/2^320%2^32
Count m while _/2^320%2^32*0^m {
Count j while _/2^(_/2^320%2^32*8)%2^8*0^j {
Count e while e-_/256^(_/2^320%2^32+1)%2^32 {
Write _/256^(_/2^320%2^32+e+5)%2^8
}
}
Count j while (1-_/2^(_/2^320%2^32*8)%2^8)*0^j {
_+(_/256^(_/2^320%2^32+1)%2^32%2^31+_/256^(_/2^320%2^32+1)%2^32/2^31-_/2^64%2^32)*2^64
Write 45)*(_/256^(_/2^320%2^32+1)%2^32/2^31
Count i while 10-i {
Write 48+_/2^64%2^32/10^(9-i)%10)*0^0^(_/2^64%2^32/10^(9-i)+0^(9-i)
}
}
}
_-_/2^(_/2^256%2^32*8)%2^32*2^(_/2^256%2^32*8)-8^86
Write 32)*0^0^(_/256^(_/2^256%2^32-4)%2^32
}
Count l while 0^(2-_/2^(_/2^320%2^32*8)%2^8)*0^l {
_+(_/256^(_/2^320%2^32+5)%2^32+2^32*(_/256^(_/2^320%2^32+1)%2^32)-_/2^((_/2^256%2^32-4)*8)%2^64)*256^(_/2^256%2^32-4)
Write 40)*0^((2-_/2^(_/256^(_/2^256%2^32)%2^32*8)%2^8)*(_/256^(_/2^256%2^32)%2^32)
_+8^86
}
}
Write 10
_-_/2^512%2^64*2^512-2^227
}
_+(_/256^(_/2^32%2^32+5)%2^32*256^(_/2^224%2^32+4)+(12-_/256^(_/2^32%2^32+1)%2^64)*256^(_/2^32%2^32+1))*0^(_/2^96%2^32-7)^2
Count p while 4479/2^(_/2^96%2^32)%2*0^p {
_+(2+_/4^80%2^64*2^8)*2^(_/2^352%2^32*8)
_+(12/2^(_/2^96%2^32)%2*(_/256^(_/4^80%2^32-7+4*(_/2^96%2^32))%2^32)+0^(_/2^96%2^32-1)^2*(_/2^352%2^32)-_/2^384%2^32)*2^384+9*2^352
Count r while 48/2^(_/2^96%2^32)%2*0^r {
_+(_/256^(_/4^80%2^32+1)%2^32+2^32*(_/256^(_/4^96%2^32+1)%2^32)-_/2^448%2^64)*2^448
_+(2^31+(_/2^448%2^32%2^31*(-1)^(_/2^448%2^32/2^31)-_/2^448%2^32/2^31)-(_/2^480%2^32%2^31*(-1)^(_/2^480%2^32/2^31)-_/2^480%2^32/2^31)-_/2^32%2^32)*2^32
_+(_/2^96%2^32/5*-(1/(3*(_/2^32%2^32-2^31)+2))+4/(_/2^96%2^32)*((_/2^32%2^32-2^31)*(-1)^(-(1/(3*(_/2^32%2^32-2^31)+2)))+(2^31-1)*-(1/(3*(_/2^32%2^32-2^31)+2))))*256^(_/2^352%2^32+1)
}
Count r while 0^(6-_/2^96%2^32)^2*0^r {
_+(_/2^352%2^32-_/2^256%2^32)*2^256
Count g while _/2^32%2^32*(_/2^256%2^32/(_/2^352%2^32)) {
_-0^(_/4^80%2^32-_/4^96%2^32)^2*(_/2^(_/2^256%2^32*8)%2^64*256^(_/2^256%2^32)+2^259)
Count k while (_/4^80%2^32-_/4^96%2^32)*0^k {
_+(0^(0^(_/4^80%2^32)+0^(_/4^96%2^32))*0^(_/256^(_/4^80%2^32)%2^8-_/256^(_/4^96%2^32)%2^8)^2-_/2^32%2^32)*2^32
Count l while _/2^32%2^32*0^l {
_+((0^(_/256^(_/4^80%2^32+1)%2^32-_/256^(_/4^96%2^32+1)%2^32)^2-_/2^32%2^32)*2^32-_/2^(_/2^256%2^32*8)%2^64*256^(_/2^256%2^32)-2^259)*0^(_/256^(_/4^80%2^32)%2^8)
Count m while 0^(_/256^(_/4^80%2^32)%2^8-1)^2*0^m {
_-_/2^(_/2^256%2^32*8)%2^64*2^(_/2^256%2^32*8)-2^259-0^0^((_/256^(_/4^80%2^32+1)-_/256^(_/4^96%2^32+1))%256^(_/256^(_/4^80%2^32+1)%2^32+4))*2^32
}
_+((_/256^(_/4^80%2^32+5)%2^32+2^32*(_/256^(_/4^96%2^32+5)%2^32)+2^64*(_/256^(_/4^80%2^32+1)%2^32)+2^96*(_/256^(_/4^96%2^32+1)%2^32)-_/2^(_/2^256%2^32*8)%4^64)*256^(_/2^256%2^32)+2^259)*0^(_/256^(_/4^80%2^32)%2^8-2)^2
}
}
_*0^0^(_/2^32%2^32)+_%256^(_/2^352%2^32)*0^(_/2^32%2^32)
_+(_/2^(_/2^256%2^32*8)%2^64-_/4^80%2^64)*4^80
}
_+0^0^(_/2^32%2^32)*256^(_/2^352%2^32+1)
}
_+(4352/2^(_/2^96%2^32)%2*(_/4^80%2^32)+112/2^(_/2^96%2^32)%2*(_/2^352%2^32))*2^384+5*2^352
Count e while _/256^(_/256^(_/2^224%2^32)%2^32+1)%2^32/64*0^(_/256^(_/2^224%2^32+4)%2^32)+0^e {
_-_/2^(_/2^224%2^32*8)%2^64*2^(_/2^224%2^32*8)-2^227
}
Count h while 0^0^(_/2^320%2^32)*(_/256^(_/2^320%2^32+5)%2^32)+0^h {
_+(_/256^(_/2^320%2^32+5)%2^32*0^0^h+_/256^(_/2^224%2^32)%2^32*0^h-_/2^320%2^32)*2^320
}
_+(2+_/2^384%2^32*2^8)*2^(_/2^352%2^32*8)+_/2^352%2^32*256^(_/2^320%2^32+5+(_/2^224%2^32-5)*0^(_/2^320%2^32))+9*2^352
_+(_/256^(_/256^(_/2^224%2^32+4)%2^32+5)%2^32-_/256^(_/2^224%2^32+4)%2^32)*256^(_/2^224%2^32+4)
}
Count p while 0^(_/2^96%2^32-9)^2*0^p {
_+(_/256^(_/256^(_/256^(_/4^64%2^32+5)%2^32+5)%2^32+1)%2^32-_/2^384%2^32)*2^384
_+(_/4^96%2^32-_/2^384%2^32)*0^0^(_/4^80%2^32*(_/256^(_/4^80%2^32)%2^40))*2^384
_+((_/2^(_/2^224%2^32*8)%2^64-_/256^(_/2^224%2^32-8)%4^64)*256^(_/2^224%2^32-8)-2^227)*0^(0^0^(_/256^(_/2^224%2^32-8)%2^32)*(_/256^(_/256^(_/2^224%2^32-8)%2^32+1)%2^32)-12)^2
_+(12-_/2^((_/2^32%2^32+1)*8)%2^64)*256^(_/2^32%2^32+1)+(2+_/2^384%2^32*2^8)*2^(_/2^352%2^32*8)+_/2^352%2^32*2^((_/2^224%2^32+4)*8)+9*2^352
}
Count s while 0^(_/2^96%2^32-10)^2*0^s {
Count h while _/256^(_/2^448%2^32+5)%2^32+0^h {
_+((_/256^(_/2^448%2^32+5)%2^32+2^32*(_/256^(_/2^480%2^32+5)%2^32))*0^0^h+3710851744617*0^h-_/2^448%2^64)*2^448
}
_+(2+2^8*(_/4^80%2^32)+2^73+2^80*(_/4^96%2^32)-_/2^(_/2^352%2^32*8)%4^72)*2^(_/2^352%2^32*8)+(_/2^352%2^32+9)*256^(_/2^480%2^32+5)+_/2^352%2^32*256^(_/2^448%2^32+5)+9*2^353
_+(12-_/256^(_/2^32%2^32+1)%2^32)*256^(_/2^32%2^32+1)-_/2^((_/4^64%2^32+5)*8)%2^32*2^((_/4^64%2^32+5)*8)
}
Count p while _/2^96%2^32/64*0^p {
_+((_/2^(_/2^224%2^32*8)%2^64-_/256^(_/2^224%2^32-16)%4^96)*256^(_/2^224%2^32-16)-2^228)*0^0^(0^0^(_/256^(_/2^224%2^32-16)%2^32)*(_/256^(_/256^(_/2^224%2^32-16)%2^32+1)%2^32)/64*0^(12-0^0^(_/256^(_/2^224%2^32-8)%2^32)*(_/256^(_/256^(_/2^224%2^32-8)%2^32+1)%2^32))^2)
_+(_/2^352%2^32+_/256^(_/2^96%2^32+5)%2^32*2^32)*256^(_/2^224%2^32+8)+(2+12*2^8)*2^(_/2^352%2^32*8)+(_/2^352%2^32-_/2^384%2^32)*2^384+9*2^352+2^227
}
}

Try it online!

It's done. I'm tired. This took four weeks.

On an older version of this, running the final test case in optimised mode (using a byte array instead of a massive int, see below) took 17 minutes 39 seconds and allocated 58MB of heap space (actually 486MB of RAM). I don't have the patience to run the unoptimised version, but as it has to perform arithmetic on 100-million-digit ints millions of times, I'd be surprised if it finished in under a year. Python already struggles to even allocate one of these values, let alone perform arithmetic on it.

Acc++

Writing this project purely in Acc!! would've been a nightmare, so I created a language called Acc++ that compiles to Acc!!. You can find the interpreter/compiler for it, alongside other test files, in the github repository for this project. Its main features are comments, a hacked-together macro system using the python ast module, and an optimised mode which uses a byte array instead of a massive int, speeding things up by many orders of magnitude.

This program basically treats the accumulator _, an arbitrarily large integer that's the only way to store data, as an arbitrarily long array of bytes. By dividing it by the appropriate power of 2 with _/2^(x*8)%2^32, I can fairly easily read bytes/32-bit words, and then multiply by said power of 2 again to write them with _+(v-_/2^(x*8)%2^32). I can define macros readWord, writeWord, readByte, writeByte to do these. One nice consequence of this is that, as long as these are the only ways I'm interacting with memory, I can add a flag to replace these with custom functions that interact with a byte array, speeding things up a huge amount. I ended up adding a bunch more of these memory-access builtins for various purposes.

The program memory is split into three parts: 64 bytes of register space containing 16(ish) 4-bit registers, 800 bytes of stack space and an unbounded heap. While most of the registers are general-purpose, a few have specific purposes:

  • hp points to just after the end of the heap, and gets increased any time something's allocated.
  • sp points to the top of the stack during evaluation.
  • sp2, when in use, points to the top of the "second stack" - an extra stack allocated on top of the heap, used for printing and checking equality.
  • rf stores the source code during evaluation.

I also created a few useful macros. Acc!!'s while loop syntax increments a variable starting from 0 and loops while a certain expression is true:

Count i while [expr] {
[code]
}

So I can make an if statement with:

Count i while [expr]*0^i {
    [code]
}

(0^i is 1 if i is 0 and 0 otherwise. I've used it a lot). Acc!! doesn't allow nested while loops using the same variable, so I've defined If, If2 ... If10 all with different variables to get around this. I've also defined While through While5 for similar reasons. The other utility macros I've defined are conc, which executes a sequence of statements, and mov, which is effectively a writeWord(x, readWord(y)).

You can find the Acc++ source for this project in def.acc, although be aware that it's a hacky mess and contains a ton of obsolete code. It's loosely commented, but probably not particularly readable, especially with all the golfing optimisations I've added.

Storage

Since tinylisp is a dynamically-typed language, values need to include their types. I ended up with the following format:

  • Integers are stored as a 0x00 byte followed by a signed 32-bit int. The high bit of the int controls whether it's negative, so this supports signed ints between -2^31 and 2^31-1, which coincedentally is exactly what this challenge requires. Overflowing them will probably break things.
  • Strings (names) are stored as an 0x01 byte followed by a 4-byte length and the rest of the string. One nice thing about tinylisp is that strings are immutable, and furthermore all strings that'll ever be used at runtime are allocated during parsing.
  • Builtins are stored as register-space pointers (more on that later)
  • All lists are stored as linked lists - an 0x02 byte followed by a 32-bit head (first item) pointer and a 32-bit tail (rest of the list) pointer. There are several advantages to this:
    • Tinylisp inherently uses linked lists - the head and tail operations correspond to the same parts of a linked list, and the cons operation constructs a list with a given head and tail pointer.
    • Linked lists have to be (in most cases) immutable since one tail pointer can be pointed to by multiple head pointers. This means we never have to value about memory management, which is both a blessing and a curse - There are several places in this program where I allocate linked lists that are later impractical to clean up, and this results in slightly ridiculous memory usage. The tail of the last item of a list is a null pointer (see below) - for example, (3 5 4) is stored as [3, [5, [4, NULL]]].
    • While initially I planned to use traditional arrays for this, they're extremely poorly suited to the task due to memory management being a pain. One other decision I've made is that all empty lists () are referred to by the NULL pointer - a pointer to address 0. While this has quite a few advantages, primarily making it easy to check if a value is null, it does result in some weird shenaniganry any time I want to append to a list, which thankfully isn't particularly often.

Evaluation

Since, as before, we have no access to any sort of recursion, we have to emulate a call stack. Each stack frame is a pair of (pointers to) lists: A list of evaluated arguments, and a list of unevaluated arguments. When evaluating a list (S-expression), the evaluated arguments list starts at (), and the unevaluated arguments list starts with the list. One quirk of this is, as the call stack is set up to only evaluate lists, we can't directly evaluate non-list values, but I've found ways around that.

To evaluate a value from the arguments list:

  • If it's an integer or NULL, it's passed as normal.
  • If it's a string, it's looked up, first in the local dictionary (obtained by looking upwards through the call stack until we hit a function), and then in the global dictionary. An undefined variable name will (probably) return null.
  • If it's a list, we push () and it onto the call stack.

The first item of a list is the function to call, and is always evaluated. After that, we check if it's a macro (q or starts with an empty list), and if so push the rest of its arguments to the evaluated stack. While i and d are also macros, they need some special handling:

  • i needs to have its first argument, the condition, evaluated regardless, but once that's been evaluated we push the other two arguments to the evaluated stack.
  • d needs to have its first argument (the name) left unevaluated, so we push that to the evaluated stack unchanged, but it does need the second argument (the value) evaluated, so we leave that alone.

One trick I've used is to have builtins point to addresses in the register space: c points to address 1, h to address 2, etc. This makes it easy to distinguish builtins from regular functions, and also makes it very easy to concisely check what a builtin is.

Also, I mentioned before that I can't directly evaluate non-list values. To get around this, I've defined two builtins that are only accessible by the interpreter itself:

  • print (address 11) is used at the top-level to print the result of an expression. It uses a fairly simple algorithm with the second stack to print a value. If given a builtin, it'll attempt to print the value at that index, which is undefined behaviour.
  • id (address 12) simply takes one argument and evaluates it. While this sounds useless, it's actually quite powerful: Certain builtins can rearrange the call stack to control what they evaluate, by replacing calls to themselves with a call to id. I'll explain this more shortly.

Once a function call has had all its arguments evaluated (/ otherwise dealt with), we check what the first argument, the function, is. If it's a builtin:

  • head, tail, and cons correspond to getting the head/tail of a linked list and constructing a new list respectively, all of which are operations I've already defined.
  • subtract and lessthan are also quite simple to implement. However, they share a lot of the same code: They both need to subtract their arguments, then s allocates a new int with that value, and l allocates a new int with whether that value is negative.
  • Since equal has to perform a deep equality check, it uses the "second stack" allocated on top of the heap. For performance reasons and to catch nulls, it first checks if the pointers to two values are equal, meaning that checking two equivalent builtins for equality will actually work. Checking a builtin and any other value (including another builtin) could return 1, return 0, or crash depending on the exact values of certain registers at the time.
  • q (and id) simply return their argument.

If one of the above is called, its return value is pushed to the evaluated args list of the previous stack frame, and the expression itself is popped from the evaluated args list. The remainder of the builtins require special handling:

  • d needs to not be able to redefine existing values. As dictionary lookup starts from the beginning, prepending the given name and value to the global dictionary would actually succeed, so we instead append them, and then return the given name wrapped in id.
  • v simply replaces itself with a call to id.
  • i checks if its first argument is truthy (not NULL, not the integer 0), and replaces itself with a call to id containing either the second or third argument depending on the first. For TCO reasons (see below), if it's itself the result of a call to id we squash that call.

Otherwise, the value is a function, and the eval'd stack is structured as ((names body) args). We push body to the stack wrapped in a call to id. When looking up the local dictionary, if names is a singleton list, we wrap it and args in a list to treat it as a single variable.

This makes TCO very easy - we can simply check if the item two steps back on the stack is a function call, and the item after that is a call to id, and if so we can simply remove both of those calls.

Then, to evaluate a piece of code, we simply parse it (the parser uses a simple one-pass algorithm) and evaluate+print all its expressions.

Golfing

It's difficult to portray just how much effort went into golfing this. The original compiled version was around 30KB, and I've spent over two weeks golfing it down to the 9.5KB version here. I'll list some of the tricks I used here.

Before I start, to put some sizes into context:

  • Reading the cheapest register, ra, is 10 bytes: _/2^32%2^32. Some of the other registers are 11 bytes due to having a 3-digit address.
  • Writing to ra is 22 bytes: _+(v-_/2^32%2^32)*2^32. For the same reasons as above, some registers are 24 bytes.
  • An if statement has an overhead of 25 bytes: Count v while (cond)*0^v {\ncode\n}

Also, because I'm using a macro system, macros that use an argument multiple times will simply have that argument placed multiple times within their code. For example, I have the following macro head_ll(x) that gets the head pointer of a linked list:

#defm head_ll(x) 0^0^x * readWord(x+1)

However, this specific macro is null-safe: The 0^0^x* at the start ensures that the macro returns 0 when given 0 (which isn't guaranteed otherwise), but this comes at the cost of having to use x twice. If an already large expression is passed to head_ll, it'll double in length, and stacking head_ll multiple times blows up the size exponentially. There are two ways around this - one way is to, where possible, use the following unsafe version that only uses x once:

#defm u_head_ll(x) readWord(x+1)

The other way is to extract expensive expressions into registers:

writeWord(ra, [expensive expression])
head_ll(rax)

First, the code generated by Acc++ isn't particularly golfed, containing a ton of whitespace. Because of this, I wrote an intermediate compressor that removes whitespace and replaces a few common patterns with shorter versions.

One important thing is that we're not limited to just 32-bit read/writes. By simply replacing 32 with 64 in read/write calls, I can read/write two words at once for almost the same byte count. When reading/writing to consecutive addresses, this both saves a ton of bytes and improves performance (since the overhead of interacting with a massive int is so large). For example, as the registers ra and rb are consecutive in memory, I can replace writeWord(ra, 5) ; writeWord(rb, 6) with write_dword(ra, 5, 6) and save a ton of bytes in the compiled result.

Perhaps unsurprisingly, though, the largest golfs came from logic optimisations. For example, I need to search through both the local and global dictionaries when looking up a variable name. Originally, this required two separate but almost-identical pieces of code to search through the dictionaries, but by looping twice, searching through the local dictionary on the first iteration and the global dictionary on the second iteration, I can remove almost half the code.

Another of these logic optimisations is initialising the global dictionary. It's stored as two linked lists: one of names and one of values. Initially, I had a ton of code to allocate linked lists, but by carefully constructing it with a for loop (see the first three lines) I've saved almost a kilobyte.

Another trick I used is something I call parallel writes. All calls involving writing memory amount to _+[x]. If I have multiple write calls in a row that don't rely on each other _+[x], _+[y], _+[z] I can merge them into _+[x]+[y]+[z], saving two bytes per use. This doesn't sound like much, but it has another advantage.

As I mentioned before, an if statement has an overhead of 25 bytes. If I have a paralellised write call _+[x]+[y]+[z] wrapped in a conditional, I can remove the if statement entirely and replace it with _+([x]+[y]+[z])*[cond]. This saves 22 bytes - 20 if cond needs to be in brackets, and 16 if cond needs to be converted to 1 or 0. (also, I randomly discovered you can make write calls conditional while messing around yesterday, which saved ~120 bytes)

One of the last things I did was switch to length-prefixed strings. With null-terminated strings, to check if two strings are equal you have to iterate through every character until you get to the end of one string. But with length-prefixed strings, I can simply read the entirety of the string as a massive int, and check if those are equal, saving several hundred bytes (since checking string equality is used twice, once in dict lookup and once in equal).

Aside from that, there have been tons of microoptimisations - removing a byte or two from conditions, simplifying a write call, shortening mathematical expressions here and there, etc. At this point, although there are definitely a few more microoptimisations I haven't found, I'd say this is fairly well-golfed, and I doubt under 9KB is possible without significantly rewriting certain parts of the program.

\$\endgroup\$
21
\$\begingroup\$

Python 2, 685 675 660 657 646 642 640 bytes

import sys,re
E=[]
G=zip("chtsle",[eval("lambda x,y=0:"+f)for f
in"[x]+y (x+[E])[0] x[1:] x-y +(x<y) +(x==y)".split()])
def V(e,L=E):
 while 1:
    try:return e and int("0%s"%e)
    except:A=e[1:]
    if""<e:return dict(G+L).get(e,e)
    f=V(e[0],L)
    if""<f:
     if f in"iv":t=V(A[0],L);e=(e[~bool(t)],t)[f>"u"];continue
     if"e">f:G[:]+=(A[0],V(A[1],L)),
     return A[0]
    if[]>f or f[0]:A=[V(a,L)for a in A]
    if[]>f:return f(*A)
    P,e=f[-2:];L=([(P,A)],zip(P,A))[P<""]
F=lambda x:V<x<""and"(%s)"%" ".join(map(F,x))or"%s"%x
for t in re.sub("([()])"," \\1 ",sys.stdin.read()).split():
 if")"==t:t=E.pop()
 if"("==t:E+=[],
 elif E:E[-1]+=t,
 else:print F(V(t))

Reads input from STDIN and writes output to STDOUT.

Although not strictly required, the interpreter supports nullary functions and macros, and optimizes tail calls executed through v.

Explanation

Parsing

To parse the input, we first surround each occurence of ( and ) with spaces, and split the resulting string into words; this gives us the list of tokens. We maintain an expression stack E, which is initially empty. We scan the tokens, in order:

  • if we encounter a (, we push an empty list at the top of the expression stack;
  • if we encounter a ), we pop the value at the top of the expression stack, and append it to the list that was previously below it on the stack;
  • otherwise, we append the current token, as a string, to the list at the top of the expression stack (we keep integers as strings at this stage, and parse them during evaluation.)

If, when processsing an ordinary token, or after popping an expression from the stack due to ), the expression stack is empty, we're at a top-level expression, and we evaluate the value we'd otherwise have appended, using V(), and print its result, formatted appropriately using F().

Evaluation

We maintain the global scope, G, as a list of key/value pairs. Initially, it contains only the builtin functions (but not the macros, and not v, which we treat as a macro), which are implemented as lambdas.

Evaluation happens inside V(), which takes the expression to evaluate, e, and the local scope, L, which is, too, a list of key/value pairs (when evaluating a top-level expression, the local scope is empty.) The guts of V() live inside an infinite loop, which is how we perform tail-call optimization (TCO), as explained later.

We process e according to its type:

  • if it's the empty list, or a string convertible to an int, we return it immediately (possibly after conversion to int); otherwise,

  • if it's a string, we look it up in a dictionary constructed from the concatenation of the global and local scopes. If we find an associated value, we return it; otherwise, e must be the name of a builtin macro (i.e. q, i, d or v), and we return it unchanged. Otherwise, if e is not a string,

  • e is a (nonempty) list, i.e., a function call. We evaluate the first element of the list, i.e., the function expression, by calling V() recursively (using the current local scope); we call the result f. The rest of the list, A, is the list of arguments. f can only be a string, in which case it's a builtin macro (or the function v), a lambda, in which case it's a builtin function, or a list, in which case it's a user-defined function or macro.

    If f is a a string, i.e., a builtin macro, we handle it in-place. If it's the macro i or v, we evaluate its first operand, and either select the second or third operand accordingly, in the case of i, or use the result of the first operand, in the case of v; instead of evaluating the selected expression recursively, which would defeat TCO, we simply replace e with the said expression, and jump to the beginning of the loop. If f is the macro d, we append a pair, whose first element is the first operand, and whose second element is the result of evaluating the second operand, to the global scope, G, and return the first operand. Otherwise, f is the macro q, in which case we simply return its operand directly.

    Othrtwise, if f is a lambda, or a list whose first element is not (), then it's a non-nullary function, not a macro, in which case we evaluate its arguments, i.e., the elements of A, and replace A with the result.

    If f is a lambda, we call it, passing it the unpacked arguments in A, and return the result.

    Otherwise, f is a list, i.e., a user-defined function or macro; its parameter list is the second-to-last element, and its body is the last element. Like in the case of the macros i and v, in order to perform TCO, we don't evaluate the body recursively, but rather replace e with the body and continue to the next iteration. Unlike i and v, however, we also replace the local scope, L, with the new local scope of the function. If the parameter list, P, is, in fact, a list, the new local scope is constructed by zipping the parameter list, P, with the argument list, A; otherwise, we're dealing with a variadic function, in which case the new local scope has only one element, the pair (P, A).

REPL

If you want to play with it, here's a REPL version of the interpreter. It supports redefining symbols, and importing files through either the command line arguments, or the (import <filename>) macro. To exit the interpreter, terminate the input (usually, Ctrl+D or Ctrl+Z).

try:import sys,re,readline
except:0
E=[];G=zip("chtsle",[eval("lambda x,y=0:"+f)for f
in"[x]+y (x+[E])[0] x[1:] x-y +(x<y) +(x==y)".split()])
def V(e,L=E):
 while 1:
    try:return e and int("0%s"%e)
    except:A=e[1:]
    if""<e:return dict(G+L).get(e,e)
    f=V(e[0],L)
    if""<f:
     if f in"iv":t=V(A[0],L);e=(e[~bool(t)],t)[f>"u"];continue
     if"e">f:G[:]+=(A[0],V(A[1],L)),
     elif"j">f:X(open(str(A[0])).read())
     return A[0]
    if[]>f or f[0]:A=[V(a,L)for a in A]
    if[]>f:return f(*A)
    P,e=f[-2:];L=([(P,A)],zip(P,A))[P<""]
F=lambda x:V<x<""and"(%s)"%" ".join(map(F,x))or"%s"%x
def X(s,v=0):
 for t in re.sub("([()])"," \\1 ",s).split():
    if")"==t:t=E.pop()
    if"("==t:E[:]+=[],
    elif E:E[-1]+=t,
    else:
     x=V(t)
     if v:print F(x)
for f in sys.argv[1:]:X("(g %s)"%f)
while 1:
 try:X(raw_input(">."[[]<E]*3+" "),1)
 except EOFError:break
 except KeyboardInterrupt:E=[];print
 except Exception as e:print"Error: "+e.message

And here's an example session, implementing merge sort:

>>> (d let d) (d if i) (d head h) (d tail t) (d prepend c) (d less l)
let
if
head
tail
prepend
less
>>>
>>> (let list (q (x... x...)))
list
>>> (let lambda (q (() (params body) (list params body))))
lambda
>>> (let def (q (() (name params body) (
...     v (list (q let) name (list (q lambda) params body))
... ))))
def
>>>
>>> (def else(body) body)
else
>>> (def or(x y) ( if x x y ))
or
>>> (def and(x y) ( if x y x ))
and
>>>
>>> (def front-half(L) ( front-half/impl L L ))
front-half
>>> (def front-half/impl(L M) (
...     if M (
...         prepend (head L)
...                 (front-half/impl (tail L) (tail (tail M)))
...     ) (else
...         ()
...     )
... ))
front-half/impl
>>>
>>> (def back-half(L) ( back-half/impl L L ))
back-half
>>> (def back-half/impl(L M) (
...     if M (
...         back-half/impl (tail L) (tail (tail M))
...     ) (else
...         L
...     )
... ))
back-half/impl
>>>
>>> (def merge(L M comp) (
...     if (and L M) (
...         if (comp (head M) (head L)) (
...             prepend (head M) (merge L (tail M) comp)
...         ) (else (
...             prepend (head L) (merge (tail L) M comp)
...         ))
...     ) (else (
...         or L M
...     ))
... ))
merge
>>>
>>> (def sort(L comp) (
...     if (and L (tail L)) (
...         merge (sort (front-half L) comp)
...               (sort (back-half L) comp)
...               comp
...     ) (else
...         L
...     )
... ))
sort
>>>
>>>
>>> (let my-list (list 4 7 2 5 9 1 6 10 8 3))
my-list
>>> my-list
(4 7 2 5 9 1 6 10 8 3)
>>> (sort my-list less)
(1 2 3 4 5 6 7 8 9 10)
>>> (sort my-list (lambda(x y) ( less y x )))
(10 9 8 7 6 5 4 3 2 1)

\$\endgroup\$
3
  • \$\begingroup\$ You can get something ever shorter using zlib :) Compress your code converted in bytes, and replace it with : import zlib;exec(zlib.decompress(your_code_compressed_in_bytes)) \$\endgroup\$
    – Labo
    Commented Nov 9, 2015 at 12:48
  • \$\begingroup\$ You could save two bytes by assigning A[0] to some one-char variable just after except block \$\endgroup\$ Commented Nov 10, 2015 at 11:32
  • \$\begingroup\$ @HannesKarppila That's right, but this would break nullary functions (since A is empty in this case), and I don't want to "regress". \$\endgroup\$
    – Ell
    Commented Nov 10, 2015 at 16:13
11
\$\begingroup\$

C (GNU), 1095 bytes

Much of the action takes place in the giant v function. Instead of implementing tail recursion explicitly, v is structured so that many of the calls from v to v will be handled by gcc's tail recursion optimization. There is no garbage collection.

This makes heavy use of GCC extensions, so it could only be compiled with gcc (use the command gcc -w -Os tl.c). It also uses some scanf extensions which were not available on Windows, which I usually use. The prospect of writing the parser with standard scanf was so awful that I used a Linux VM to test the program instead. Parsing without scanf character classes probably would have added 100+ bytes.

#define O(...)({o*_=malloc(32);*_=(o){__VA_ARGS__};_;})
#define P printf
#define F(I,B)({for(I;x->c;x=x->l)B;})
#define Z return
typedef struct o{struct o*n,*l,*c;int i,t;}o;E(o a,o b){Z
a.n?!strcmp(a.n,b.n):a.c?b.c&&E(*a.c,*b.c)&E(*a.l,*b.l):!b.c&a.i==b.i;}p(o*x){x->t?P("%d ",x->i):x->n?P("%s ",x->n):F(P("("),p(x->c);P(")"));}o*x,G,N;*C(o*h,o*t){Z
O(c:h,l:t);}o*v(o*x,o*e){o*W(o*l,o*e){Z
l->c?C(v(l->c,e),W(l->l,e)):&N;}o*y,*a,*f;int t;Z
x->c?y=v(x->c,e),x=x->l,t=y->i,t?9/t?a=v(x->c,e),t>7?(t>8?a->c:a->l)?:a:t>6?v(a,e):t<6?x=v(x->l->c,e),t>4?C(a,x):O(t:1,i:t>3?E(*a,*x):t>2?a->i<x->i:a->i-x->i):v((a-&N&&!a->t|a->i?x:x->l)->l->c,e):(t&1&&d(x->c->n,v(x->l->c,e)),x->c):(y->l->l->l?y=y->l:(x=W(x,e)),a=y->c,v(y->l->c,a->n?O(n:a->n,c:x,l:&G):F(f=&G,(f=O(n:a->c->n,c:x->c,l:f),a=a->l);f))):x->n?e->n?strcmp(x->n,e->n)?v(x,e->l):e->c:e:x;}d(o*n,o*x){*v(O(n:""),&G)=(o){n:n,c:x,l:O()};}*R(h){char*z,*q;Z
scanf(" %m[^ \n()]",&q)>0?h=strtoul(q,&z,10),C(*z?O(n:q):O(t:1,i:h),R()):~getchar()&1?q=R(),C(q,R()):&N;}main(i){for(;++i<12;)d(strndup("slecivthqd"+i-2,1),O(i:i));F(x=R(),p(v(x->c,&G)));}

Semi-ungolfed

typedef struct o o;
struct o {
    char* n;
    o* l, //next in this list
     * c; 
    int i,
        t;
} ;



#define O(...)({o*_=malloc(32);*_=(o){__VA_ARGS__};_;})

E(o a, o b) { //tests equality 
    return
        a.n ? !strcmp(a.n,b.n) :
        a.t ? a.i==b.i :
        a.c ? b.c && E(*a.c,*b.c)&E(*a.l,*b.l) :
        !b.c
    ;
}

#define P printf


p(o*x){
    x->t?P("%d ",x->i):x->n?P("%s ",x->n):({for(P("(");x->c;x=x->l)p(x->c);P(")");});
}


o*_,G,N; //N = nil



o*C(o*h,o*t){return O(c:h,l:t);}


/*
        2 3 4 5 6 7 8 9 10 11
        s l e c i v t h d  q
    */


o* v(o* x, o* e) { //takes list, int, or name
    o*W(o* l, o* e) { //eval each item in list
        return l->c ? C(v(l->c ,e), W(l->l, e)) : &N;
    }

    o*y,*a,*f;int t;
    return x->c ? //nonempty list = function/macro call
        y = v(x->c,e), //evals to function/macro
        x = x->l,   //list position of first arg (if it exists)
        (t=y->t)?   //builtin no., if any
             t>9 ?
              t&1 ? x->c // 11 = q
                : //10 = d
                (d(x->c,v(x->l->c,e)),x->c)
           : (a = v(x->c,e), //eval'd first arg
             t)>7 ? // t/h
                (t > 8 ? a->c : a->l) ?: a
           : t>6 ? //v
                v(a,e)
           : (x = x->l, //position of 2nd arg in list
             t)>5 ? //i
                v( (a->n||a->l||a->i|a->t>1 ? x : x->l)->c, e)
           : (x = v(x->c,e), //evaluated 2nd arg
             t)>4 ? // c
                C(a,x)
           : O(t:1,i:
                t>3 ? E(*a,*x) :  //e
                t>2 ? a->i<x->i : //l
                      a->i-x->i   //s
              )
        :
        (
            y->l->l->l ? //whether this is macro
                y = y->l :
                (x = W(x,e)),  //eval args
            a = y->c,  //a = arg list
            //a = a->n ? x=C(x, &N), C(a, &N) : a, //transform variadic style to normal
            v(y->l->c,
               a->n ? //variadic
                O(n:a->n,c:x,l:&G)
              : ({
                   for(f=&G; a->c; a=a->l,x=x->l)
                      f=O(n:a->c->n, c: x->c, l:f);
                   f;
                })
            )
        )
    :
    x->n ? // name
        e->n ?
            strcmp(x->n,e->n) ?
                v(x,e->l)
            : e->c
        : e
     : x; //int or nil
}

d(o*n,o*x){
    * v(O(n:""),&G) =
        (o){n:n->n,c:x,l:O()};
}


;
o*R(){
    char*z,*q;int h;
return scanf(" %m[^ \n()]",&q)>0?
    h=strtoul(q,&z,10),
    C(*z ? O(n:q) : O(t:1,i:h), R())
: getchar()&1?&N:(q=R(),C(q,R()));
}
main(i) {

    for(;++i<12;) d(O(n:strndup("slecivthdq"+i-2,1)),O(t:i));

    o *q;
    for(q=R(); q->c; q=q->l) p(v(q->c,&G));

}
\$\endgroup\$
3
  • \$\begingroup\$ What is the usage of the compiled executable? Is it REPL? Does it take a filename as input? \$\endgroup\$
    – ckjbgames
    Commented Feb 24, 2017 at 22:03
  • \$\begingroup\$ @ckjbgames It reads a program from stdin. \$\endgroup\$
    – feersum
    Commented Mar 5, 2017 at 7:19
  • \$\begingroup\$ Okay. I think you should edit your answer and note that. \$\endgroup\$
    – ckjbgames
    Commented Mar 5, 2017 at 12:55
5
\$\begingroup\$

Ceylon, 2422 bytes

(I think this is my longest golfed program yet.)

import ceylon.language{sh=shared,va=variable,fo=formal,O=Object}import ceylon.language.meta.model{F=Function}interface X{sh fo V v(S t);sh fo G g;}class G(va Map<S,V>m)satisfies X{v(S t)=>m[t]else nV;g=>this;sh S d(S s,V v){assert(!s in m);m=map{s->v,*m};return s;}}V nV=>nothing;class LC(G c,Map<S,V>m)satisfies X{g=>c;v(S t)=>m[t]else g.v(t);}alias C=>V|Co;interface Co{sh fo C st();}interface V{sh fo C l(X c,V[]a);sh default Boolean b=>0<1;sh fo C vO(X c);sh default V vF(X c){va C v=vO(c);while(is Co n=v){v=n.st();}assert(is V r=v);return r;}}class L(sh V*i)satisfies V{vO(X c)=>if(nonempty i)then i[0].vF(c).l(c,i.rest)else this;equals(O o)=>if(is L o)then i==o.i else 1<0;b=>!i.empty;string=>"(``" ".join(i)``)";hash=>i.hash;sh actual C l(X c,V[]p){value[h,ns,x]=i.size<3then[f,i[0],i[1]]else[m,i[1],i[2]];value n=if(is L ns)then[*ns.i.narrow<S>()]else ns;assert(is S|S[]n,is V x);V[]a=h(c,p);LC lC=if(is S n)then LC(c.g,map{n->L(*a)})else LC(c.g,map(zipEntries(n,a)));return object satisfies Co{st()=>x.vO(lC);};}}class S(String n)satisfies V{vO(X c)=>c.v(this);l(X c,V[]a)=>nV;equals(O o)=>if(is S o)then n==o.n else 1<0;hash=>n.hash;string=>n;}class I(sh Integer i)satisfies V{vO(X c)=>this;l(X c,V[]a)=>nV;equals(O o)=>if(is I o)then i==o.i else 1<0;hash=>i;b=>!i.zero;string=>i.string;}V[]f(X c,V[]a)=>[for(v in a)v.vF(c)];V[]m(X c,V[]a)=>a;L c(X c,V h,L t)=>L(h,*t.i);V h(X c,L l)=>l.i[0]else L();V t(X c,L l)=>L(*l.i.rest);I s(X c,I f,I s)=>I(f.i-s.i);I l(X c,I f,I s)=>I(f.i<s.i then 1else 0);I e(X c,V v1,V v2)=>I(v1==v2then 1else 0);C v(X c,V v)=>v.vO(c);V q(X c,V a)=>a;C i(X c,V d,V t,V f)=>d.vF(c).b then t.vO(c)else f.vO(c);S d(X c,S s,V x)=>c.g.d(s,x.vF(c));class B<A>(F<C,A>nat,V[](X,V[])h=f)satisfies V given A satisfies[X,V+]{vO(X c)=>nV;string=>nat.declaration.name;l(X c,V[]a)=>nat.apply(c,*h(c,a));}{<S->V>*}b=>{S("c")->B(`c`),S("h")->B(`h`),S("t")->B(`t`),S("s")->B(`s`),S("l")->B(`l`),S("e")->B(`e`),S("v")->B(`v`),S("q")->B(`q`,m),S("i")->B(`i`,m),S("d")->B(`d`,m)};[V*]p(String inp){value ts=inp.split(" \n()".contains,1<0,1<0);va[[V*]*]s=[];va[V*]l=[];for(t in ts){if(t in" \n"){}else if(t=="("){s=[l,*s];l=[];}else if(t==")"){l=[L(*l.reversed),*(s[0]else[])];s=s.rest;}else if(exists i=parseInteger(t),i>=0){l=[I(i),*l];}else{l=[S(t),*l];}}return l.reversed;}sh void run(){va value u="";while(exists l=process.readLine()){u=u+l+"\n";}V[]e=p(u);X c=G(map(b));for(v in e){print(v.vF(c));}}

I could have golfed some bytes more, as I used some two-letter identifiers in some places, but I've run out of somewhat meaningful single letters for those. Although even this way it doesn't look like Ceylon very much ...

This is an object-oriented implementation.

We have a value interface V with implementing classes L (list – just a wrapper around a Ceylon sequential of V), S (symbol – wrapper around a string), I (integer – wrapper around a Ceylon integer) and B (builtin function or macro, a wrapper around a Ceylon function).

I use the standard Ceylon equality notation by implementing the equals method (and also the hash attribute, which is really only needed for symbols), and also the standard string attribute for output.

We have a Boolean attribute b (which is true by default, overridden in I and L to return false for empty lists), and two methods l (call, i.e. use this object as a function) and vO (evaluate one step). Both return either a value or a Continuation object which allows then evaluation for one more step, and vF (evaluate fully) loops until the result is not a continuation anymore.

A context interface allows access to variables. There are two implementations, G for the global context (which allows adding variables using the d builtin) and LC for a local context, which is active when evaluating the expression of a user function (it falls back to the global context).

Symbols evaluation accesses the context, lists (if non empty) evaluate by first evaluating their first element and then calling its call method. Call is implemented just by lists and builtins – it first evaluates the argument (if a function, not if a macro) and then does the actual interesting stuff – for builtins just what is hardcoded, for lists it creates a new local context and returns a continuation with that.

For the builtins I used a trick similar to what I used in my Shift Interpreter, which allows me to define them with the argument types they need, but call them with a generic sequence using reflection (the types will be checked at call time). This avoids type conversion/assertion hassle inside the functions/macros, but needs top-level functions so I can get their meta-model Function objects.

The p (parse) function splits the string at spaces, newlines and parentheses, then loops over the tokens and builds lists using a stack and a running list.

The interpreter (in the run method, which is the entry point) then takes this list of expressions (which are just values), evaluates each of them, and prints the result.


Below is a version with comments and run through a formatter.

An earlier version before I started golfing (and still with some misunderstandings about list evaluation) is found at my Github repository, I'll put this one there soon (so make sure to look at the first version if you want the original).

//  Tiny Lisp, tiny interpreter
//
// An interpreter for a tiny subset of Lisp, from which most of the
// rest of the language can be bootstrapped.
//
// Question:   https://codegolf.stackexchange.com/q/62886/2338
// My answer:  https://codegolf.stackexchange.com/a/63352/2338
//
import ceylon.language {
    sh=shared,
    va=variable,
    fo=formal,
    O=Object
}
import ceylon.language.meta.model {
    F=Function
}

// context

interface X {
    sh fo V v(S t);
    sh fo G g;
}
// global (mutable) context, with the buildins 
class G(va Map<S,V> m) satisfies X {
    // get entry throws error on undefined variables. 
    v(S t) => m[t] else nV;
    g => this;
    sh S d(S s, V v) {
        // error when already defined
        assert (!s in m);
        // building a new map is cheaper (code-golf wise) than having a mutable one.
        m = map { s->v, *m };
        return s;
    }
}

// This is simply a shorter way of writing "this is not an allowed operation".
// It will throw an exception when trying to access it.
// nV stands for "no value".
V nV => nothing;

// local context
class LC(G c, Map<S,V> m) satisfies X {
    g => c;
    v(S t) => m[t] else g.v(t);
    // sh actual String string => "[local: ``m``, global: ``g``]";
}

// continuation or value
alias C => V|Co;

// continuation
interface Co {
    sh fo C st();
}

// value
interface V {
    // use this as a function and call with arguments.
    // will not work for all types of stuff.
    sh fo C l(X c, V[] a);
    // check the truthiness. Defaults to true, as
    // only lists and integers can be falsy.
    sh default Boolean b => 0 < 1;
    // evaluate once (return either a value or a continuation).
    // will not work for all kinds of expression.
    sh fo C vO(X c);
    /// evaluate fully
    sh default V vF(X c) {
        va C v = vO(c);
        while (is Co n = v) {
            v = n.st();
        }
        assert (is V r = v);
        return r;
    }
}
class L(sh V* i) satisfies V {

    vO(X c) => if (nonempty i) then i[0].vF(c).l(c, i.rest) else this;
    equals(O o) => if (is L o) then i == o.i else 1 < 0;
    b => !i.empty;
    string => "(``" ".join(i)``)";
    hash => i.hash;

    sh actual C l(X c, V[] p) {
        value [h, ns, x] =
                i.size < 3
                then [f, i[0], i[1]]
                else [m, i[1], i[2]];
        // parameter names – either a single symbol, or a list of symbols.
        // If it is a list, we filter it to throw out any non-symbols.
        // Should throw an error if there are any, but this is harder.
        value n = if (is L ns) then [*ns.i.narrow<S>()] else ns;
        assert (is S|S[] n, is V x);
        V[] a = h(c, p);

        // local context
        LC lC = if (is S n) then
            LC(c.g, map { n -> L(*a) })
        else
            LC(c.g, map(zipEntries(n, a)));
        // return a continuation instead of actually
        // calling it here, to allow stack unwinding.
        return object satisfies Co {
            st() => x.vO(lC);
        };
    }
}

// symbol
class S(String n) satisfies V {
    // evaluate: resolve
    vO(X c) => c.v(this);
    // call is not allowed
    l(X c, V[] a) => nV;
    // equal if name is equal
    equals(O o) => if (is S o) then n == o.n else 1 < 0;
    hash => n.hash;
    string => n;
}

// integer
class I(sh Integer i) satisfies V {

    vO(X c) => this;
    l(X c, V[] a) => nV;
    equals(O o) => if (is I o) then i == o.i else 1 < 0;
    hash => i;
    b => !i.zero;
    string => i.string;
}

// argument handlers for functions or macros
V[] f(X c, V[] a) => [for (v in a) v.vF(c)];
V[] m(X c, V[] a) => a;

// build-in functions
// construct
L c(X c, V h, L t) => L(h, *t.i);
// head
V h(X c, L l) => l.i[0] else L();
// tail
V t(X c, L l) => L(*l.i.rest);
// subtract
I s(X c, I f, I s) => I(f.i - s.i);
// lessThan
I l(X c, I f, I s) => I(f.i < s.i then 1 else 0);
// equal
I e(X c, V v1, V v2) => I(v1 == v2 then 1 else 0);
// eval (returns potentially a continuation)
C v(X c, V v) => v.vO(c);

// build-in macros
// quote
V q(X c, V a) => a;
// if (also returns potentially a continuation)
C i(X c, V d, V t, V f) => d.vF(c).b then t.vO(c) else f.vO(c);
// define symbol in global context
S d(X c, S s, V x) => c.g.d(s, x.vF(c));

// buildin function or macro, made from a native function and an argument handler
class B<A>(F<C,A> nat, V[](X, V[]) h = f)
        satisfies V
        given A satisfies [X, V+] {
    vO(X c) => nV;
    string => nat.declaration.name;
    // this "apply" is a hack which breaks type safety ...
    // but it will be checked at runtime.
    l(X c, V[] a) => nat.apply(c, *h(c, a));
}

// define buildins
{<S->V>*} b => {
    S("c") -> B(`c`),
    S("h") -> B(`h`),
    S("t") -> B(`t`),
    S("s") -> B(`s`),
    S("l") -> B(`l`),
    S("e") -> B(`e`),
    S("v") -> B(`v`),
    S("q") -> B(`q`, m),
    S("i") -> B(`i`, m),
    S("d") -> B(`d`, m)
};

// parses a string into a list of expressions.
[V*] p(String inp) {
    // split string into tokens (retain separators, don't group them –
    // whitespace and empty strings will be sorted out later in the loop)
    value ts = inp.split(" \n()".contains, 1 < 0, 1 < 0);
    // stack of not yet finished nested lists, outer most at bottom
    va [[V*]*] s = [];
    // current list, in reverse order (because appending at the start is shorter)
    va [V*] l = [];
    for (t in ts) {
        if (t in " \n") {
            // do nothing for empty tokens
        } else if (t == "(") {
            // push the current list onto the stack, open a new list.
            s = [l, *s];
            l = [];
        } else if (t == ")") {
            // build a lisp list from the current list,
            // pop the latest list from the stack, append the created lisp list. 
            l = [L(*l.reversed), *(s[0] else [])];
            s = s.rest;
        } else if (exists i = parseInteger(t), i >= 0) {
            // append an integer to the current list.
            l = [I(i), *l];
        } else {
            // append a symbol to the current list.
            l = [S(t), *l];
        }
    }
    return l.reversed;
}

// Runs the interpreter.
// This handles input and output, calls the parser and evaluates the expressions.
sh void run() {
    va value u = "";
    while (exists l = process.readLine()) {
        u = u + l + "\n";
    }
    V[] e = p(u);
    // create global context
    X c = G(map(b));
    // iterate over the expressions, ...
    for (v in e) {
        // print("  '``v``' → ...");
        // ... evaluate each (fully) and print the result.
        print(v.vF(c));
    }
}
\$\endgroup\$
4
+500
\$\begingroup\$

tinylisp, 6161 1401 1060 931 bytes

(d A(q((S T)(i T(i(e S(h T))1(A S(t T)))0
(d B(q((S T)(i(e S())T(B(t S)(c(h S)T
(d C(q((S)(B S(
(d D(q((S T U)(i S(i(A(h S)(q(40 41 32 10)))(i T(i(A(h S)(q(32 10)))(D(t S)()(c(C T)U))(D(t S)()(c(h S)(c(C T)U))))(i(A(h S)(q(32 10)))(D(t S)()U)(D(t S)()(c(h S)U))))(D(t S)(c(h S)T)U))(i T(c(C T)U)U
(d E(q((S)(i S(i(l 47(h S))(i(l(h S)58)(E(t S))0)0)1
(d F(q((S T)(i T(a S(F S(s T 1)))0
(d G(q((S T)(i S(G(t S)(a(F T 10)(s(h S)48)))T
(d H(q((S)(i(E S)(G S 0)(Z(c 36 S
(d I(q((S T)(i S(i(e(h S)41)(I(t S)(c()T))(i(e(h S)40)(I(t S)(c(c(h T)(h(t T)))(t(t T))))(I(t S)(c(c(H(h S))(h T))(t T)))))T
(d J(q((S)(h(h(I S(
(d K(q((S)(J(c 41(D S()(q(40
(d L(q((S)(Z(q (32
(d M(q(S S
(d N(q((S T)(i T(c(v(M S(h T)))(N S(t T)))(
(d X(q((S)(L(N(q Q)(K S
(d Z string
(d $c c
(d $h h
(d $t t
(d $s s
(d $l l
(d $e e
(d $v v
(d $q q
(d $i i
(d $d d
(d P(q(()(S)(i(e(type S)(q Int))S(i(e(type S)(q List))(N(q P)S)(Z(t(chars S
(d Q(q((S)(disp(v(M(q P)S

Try it online!

The golfed version's TIO link includes the test cases in the footer.

Ungolfed ( also includes test cases ) :

(d in
(q((A B)
  (i
    (e B ())
    0
    (i
      (e A (h B))
      1
      (in A (t B))
    )
  )
)))

(d rev*
(q((A B)
  (i
    (e A ())
    B
    (rev* (t A) (c (h A) B))
  )
)))

(d rev
(q((A)
  (rev* A ())
)))

(d TOK
(q((L$ A B)
  (i 
    (e L$ ())
    (i 
      (e A ())
      B
      (c (rev A) B)
    )
    (i
      (in (h L$) (q(40 41 32 10)))
      (i
        (e A ())
        (i 
          (in (h L$) (q(32 10)))
          (TOK (t L$) () B)
          (TOK (t L$) () (c (h L$) B))
        )
        (i
          (in (h L$) (q(32 10)))
          (TOK (t L$) () (c (rev A) B))
          (TOK (t L$) () (c (h L$) (c (rev A) B)))
        )
      )
      (TOK (t L$) (c (h L$) A) B) 
    )
  )
)))

(d int?
(q((L$)
  (i
    (e L$ ())
    1
    (i 
      (l 47 (h L$))
      (i 
        (l (h L$) 58)
        (int? (t L$))
        0
      )
      0
    )
  )
)))

(d times
(q((X Y)
  (i
    (e Y 0)
    0
    (a X (times X (s Y 1)))
  )
)))

(d makeint
(q((L$ X)
  (i 
    (e L$ ())
    X
    (makeint (t L$) (a (times X 10) (s (h L$) 48) )) 
  ) 
)))

(d CAT
(q((L$)
  (i
    (int? L$)
    (makeint L$ 0)
    (string (c 36 L$) )
  )
)))

(d PAREN*
(q((L$ A)
  (i
    (e L$ ())
    A
    (i
      (e (h L$) 41)
      (PAREN* (t L$) (c () A))
      (i
        (e (h L$) 40)
        (PAREN* (t L$) (c (c (h A) (h (t A))) (t (t A)) ) )       
        (PAREN* (t L$) (c (c (CAT (h L$)) (h A)) (t A) ) ) 
      )
    )
  )
)))

(d PAREN
(q((L$)
  ( h( h (PAREN*  L$ () ) ) )
)))

(d READ
(q((L$)
  (PAREN (c 41 (TOK L$ () (q(40))) )  )
)))


(d DISP
(q((L$)
  (i
    (e L$ ())
    ()
    (c  
       (c (q disp) (c (c (q q) (c (h L$)  ()) )()) )
       (DISP (t L$))
    )
  )
)))

(d PRINT
(q((L$)
  (DISP L$)
)))

(d SILENT
(q((A)
  (string (q (32)))
)))


(d list
(q(args
  args
)))


(d MAP
(q((X Y)
  (i 
    (e Y ())
    ()
    (c (v (list X (h Y) )) (MAP X (t Y)))
  )
)))


(d EXEC
(q((L$)
   (SILENT (MAP (q display) (READ L$) ) ) 
)))

(d $c c)
(d $h h)
(d $t t)
(d $s s)
(d $l l)
(d $e e)
(d $v v)
(d $q q)
(d $i i)
(d $d d)

(d clean
(q(()(A)
  (i
    (e (type A) (q Int))
    A
    (i
      (e (type A) (q List))
      (MAP (q clean) A)
      (string (t (chars A)))
    )
  )
)))

(d display
(q((A)
  (disp (v (list (q clean) A)))
)))




Try it online!

Note

In what follows, I refer to the two tinylisps as tinylispI ( the implementation version where I wrote the interpreter) and tinylispT ( the target version where you write your own code). Note that the specs for each version are different.

Limitations

  1. There is no string support in tinylispI so to execute code you need to first translate that code into a list of char codes ( you can do that with this JS script ) then pass the list to EXEC.

  2. There seems to be a limit on the length of char codes that I can pass in a list, so you may need to break your codes into sections.

3. Reserved Words. Because of my implementation approach, the additional functions that are available in tinylispI are also available in tinylispT which means you can use them also if you want, but you can't use their names. the (help) command shows these functions along with the ones defined in the tinylispT spec.

Approach

My first step was to write a parser which take a list of char codes and transforms it into an S-expression. In the prototyping phase, I wrote code that takes this S-expression and evaluates it (using v), and prints the results (using disp). It may seem initially like that's all that's needed but it has this draw-back: names that the user uses could collide with names that are used in the interpreter implementation. I needed to fix that. I considered these 3 alternatives:

  1. Use globals and locals to store all names and values in S-expressions. Verdict? FAIL. Why? The tests take forever to run + there are some bugs that I couldn't resolve, the code is really complex. This is by far the coolest option, but it simply doesn't work!

  2. Since names of 100 or more are illegal, use only names that are 100 or more chars for the implementation, especially globals. This prevents name collisions. Verdict? MAYBE - draw-backs: (a) There are still some names e.g. "string" that are built-ins in tinylispI but are not in the spec for tinylispT. User could use one of those names and get an error. We would have to add a "limitation" that lists a set of reserved words.

  3. Transform the user's code so that all names have a pre-fix (e.g. '$'). Verdict? MAYBE.

Initially I went with option #2. However, I got some feedback that this approach is invalid, for the following reasons:

  1. Long names (> 100 chars) are not invalid, so there could be name collisions.
  2. built-ins (like disp) for example, are exposed so the user could use them, even though there is no mention of these in the spec.

I have switched to Option #3. Now there is a clean separation between user's code and system code. All the identifiers in the user's code are prefixed with $. I exposed exactly the ones I want exposed, by defining for example:

(d $s s)

and did that for all the built-ins from the spec. The display function I wrote maps the printed-out value back to non-prefixed, thus completing the illusion that the code is working as defined in the input string.

\$\endgroup\$
7
  • \$\begingroup\$ Very useful feed-back! I switched to Option #3. Please re-review. \$\endgroup\$ Commented Aug 21 at 18:23
  • \$\begingroup\$ Much better! I wasn't able to find a case where it breaks, so +1 from me. There is a fair bit more golfing you can do with your function and parameter names, though. ;) See also this golfing tip. \$\endgroup\$
    – DLosc
    Commented Aug 21 at 19:22
  • \$\begingroup\$ Is this answer eligible for a bounty? ( for "answering in a language that you made" ) \$\endgroup\$ Commented Aug 22 at 1:56
  • \$\begingroup\$ @AndrewBayly Yes--go ahead and edit this post to add it to the Bounty Claims section. \$\endgroup\$
    – DLosc
    Commented Aug 22 at 4:45
  • \$\begingroup\$ Hmm. I can't edit that post - I get this message: "Suggested edits are not allowed on non-tag-wiki posts on meta sites." Shall I add my claim as a comment, will that work? \$\endgroup\$ Commented Aug 22 at 16:58
3
\$\begingroup\$

Setanta, 1392 bytes

Takes a long time for test #6, even with the CLI version of Setanta.

gniomh(s){i:=0F:=fad@s
gniomh L(x){toradh go_teacs(x)[0]=="["}gniomh p(){nuair-a i<F&s[i]<"!" i+=1ma i>=F toradh!1t:=""ma s[i]=="("{t=[]i+=1nuair-a 1{ma s[i]==")"{i+=1toradh t}t+=[p()]nuair-a s[i]<"!" i+=1}}nuair-a i<F&s[i]>" "&s[i]!="("&s[i]!=")"{t+=s[i]i+=1}le i idir(0,fad@t)ma aimsigh@(go_liosta@"0123456789"())(t[i])<0 toradh t
toradh go_uimh(t)}G:=0gniomh e(x,l){nuair-a 1{ma!L(x){le j idir(0,2){le i idir(0,fad@l)ma l[i][0]==x toradh l[i][1]l=G}toradh x}ma x==[] toradh[]f:=e(x[0],l)ma f==!0{c:=e(x[1],l)x=x[(c!=[]&c&2)|3]}no{ma go_teacs(f)[0]=="<" toradh f(x,l)A:=f[fad@f-2]m:=[]ma f[0]!=m{ma L(A) le i idir(0,fad@A)m+=[[A[i],e(x[i+1],l)]]no {le i idir(0,fad@x-1)m+=[e(x[i+1],l)]m=[[A,m]]}}no{ma L(A) le i idir(0,fad@A)m+=[[A[i],x[i+1]]]no m=[[A,cuid@x(1,fad@x)]]}x=f[fad@f-1]l=m}}}G=[["c",gniomh(x,l){toradh [e(x[1],l)]+e(x[2],l)}],["h",gniomh(x,l){x=e(x[1],l)ma fad@x toradh x[0]toradh[]}],["t",gniomh(x,l){x=e(x[1],l)toradh cuid@x(1,fad@x)}],["s",gniomh(x,l){toradh e(x[1],l)-e(x[2],l)}],["l",gniomh(x,l){toradh e(x[1],l)<e(x[2],l)|0&1}],["e",gniomh(x,l){toradh e(x[1],l)==e(x[2],l)|0&1}],["v",gniomh(x,l){toradh e(e(x[1],l),l)}],["q",gniomh(x,l){toradh x[1]}],["i",!0],["d",gniomh(x,l){s:=x[1]le i idir(0,fad@G)ma G[i][0]==s 1/0G+=[[s,e(x[2],l)]]toradh s}]]gniomh h(x){toradh (go_teacs(x)[0]=="["&"("+nasc@(thar(h,x))(" ")+")")|x}nuair-a 1{s:=p()ma!1==s bris scriobh(h(e(s,[])))}}

try-setanta.ie link

Not-quite-golfed version:

gniomh(s){
    i:=0 >-- Position for parsing
    F:=fad@s >-- Length of `s`
    gniomh L(x){toradh go_teacs(x)[0]=="["} >-- Returns whether x is a list
    gniomh p(){ >-- Parse an expression
        nuair-a i<F&s[i]<"!" i+=1 >-- Skip whitespace
        ma i>=F toradh!1 >-- Return `bréag` if there’s no more code
        t:=""
        ma s[i]=="("{ >-- Parse list
            t=[]
            i+=1
            nuair-a 1{
                ma s[i]==")"{i+=1toradh t}
                t+=[p()]
                nuair-a s[i]<"!" i+=1
            }
        }
        >-- Parse atom
        nuair-a i<F&s[i]>" "&s[i]!="("&s[i]!=")"{t+=s[i]i+=1}
        le i idir(0,fad@t)ma aimsigh@(go_liosta@"0123456789"())(t[i])<0 toradh t
        toradh go_uimh(t)
    }
    G:=0 >-- List of globals, set later because we need `e`
    >-- Lookup name (inlined in golfed version)
    gniomh a(l,s){le j idir(0,2){le i idir(0,fad@l)ma l[i][0]==s toradh l[i][1]l=G}toradh s}
    gniomh e(x,l){ >-- Evaluate `x` with locals `l`
        nuair-a 1{ >-- Loop for TCO
            ma!L(x) toradh a(l,x) >-- Evaluate atom
            ma x==[] toradh[] >-- Evaluate empty list
            f:=e(x[0],l) >-- Evaluate function call
            ma f==!0{c:=e(x[1],l)x=x[(c!=[]&c&2)|3]} >-- Case for `i`
            no{
                ma go_teacs(f)[0]=="<" toradh f(x,l) >-- Some other builtin
                A:=f[fad@f-2] >-- User function or macro
                m:=[]
                ma f[0]!=m{ >-- If first element is nil, then `f` is a func
                    ma L(A) le i idir(0,fad@A)m+=[[A[i],e(x[i+1],l)]]
                    no {le i idir(0,fad@x-1)m+=[e(x[i+1],l)]m=[[A,m]]}
                }no{ >-- `f` is a macro
                    ma L(A) le i idir(0,fad@A)m+=[[A[i],x[i+1]]]
                    no m=[[A,cuid@x(1,fad@x)]]
                }
                x=f[fad@f-1] >-- Tail call
                l=m
            }
        }
    }
    G=[ >-- Populate list of globals
        ["c",gniomh(x,l){toradh [e(x[1],l)]+e(x[2],l)}],
        ["h",gniomh(x,l){x=e(x[1],l)ma fad@x toradh x[0]toradh[]}],
        ["t",gniomh(x,l){x=e(x[1],l)toradh cuid@x(1,fad@x)}],
        ["s",gniomh(x,l){toradh e(x[1],l)-e(x[2],l)}],
        ["l",gniomh(x,l){toradh e(x[1],l)<e(x[2],l)|0&1}],
        ["e",gniomh(x,l){toradh e(x[1],l)==e(x[2],l)|0&1}],
        ["v",gniomh(x,l){toradh e(e(x[1],l),l)}],
        ["q",gniomh(x,l){toradh x[1]}],
        ["i",!0], >-- Special case for TCO
        >-- Evaluate 1/0 to raise an error
        ["d",gniomh(x,l){s:=x[1]le i idir(0,fad@G)ma G[i][0]==s 1/0G+=[[s,e(x[2],l)]]toradh s}]
    ]
    >-- Stringify expression in desired format
    gniomh h(x){toradh (go_teacs(x)[0]=="["&"("+nasc@(thar(h,x))(" ")+")")|x}
    nuair-a 1{ >-- Parse and evaluate expressions in `s`
        s:=p()
        ma!1==s bris >-- If the result is equal to `bréag`, then break
        scriobh(h(e(s,[])))
    }
}
\$\endgroup\$
1
\$\begingroup\$

JavaScript (Node.js), 1800 bytes

b=a=>a.replace(/\(/g,' ( ').replace(/\)/g,' ) ').trim().split(/\s+/)
c=i=>i.match(/^\d+$/)?parseInt(i):i=='nil'?null:i
d=(i,l)=>l?(t=i.shift())?t=='('?[l.push(d(i,[])),d(i,l)][1]:t==')'?l:d(i,l.concat(c(t))):l.pop():d(i,[])
R=x=>d(b(x))
function D(o,b,e){b=b||[]
e=e||[]
this.o=o
this.d={}
for(i=0;i<b.length;i++)this.s(b[i],e[i])}
D.prototype.s=function(y,v){this.d[y]=v}
D.prototype.f=function(y){return y in this.d?this:this.o?this.o.f(y):null}
D.prototype.g=function(y){e=this.f(y);if(!e)throw "not found: "+y;return e.d[y]}
F=new D(null)
F.s('c',(a,b)=>{r=b?b.slice():[];r.unshift(a);return r})
F.s('h',(a)=>a?a.length?a[0]:null:null)
F.s('t',(a)=>a?a.length>1?a.slice(1):null:null)
F.s('s',(a,b)=>a-b)
F.s('l',(a,b)=>a<b?1:0)
F.s('e',(a,b)=>{q=(a,b)=>{if(Array.isArray(a)){if(a.length!=b.length)return 0
e=1
for(i=0;i<a.length;i++){e=e&&q(a[i],b[i])}
return e}else return a==b?1:0}
return q(a,b)})
G=(a,e)=>Array.isArray(a)?a.map(k=>E(k,e)):typeof a=='string'?e.g(a):a
E=(x,n)=>{while(1){if(Array.isArray(x)){if(!x.length){return x}else{if(x[0]=='v'){x=E(x[1],n)} else if(x[0]=='d'){var y=x[1]
var v=E(x[2],n)
n.s(y, v)
return y}else if(x[0]=='i'){var t=E(x[1],n)
if(t!=null && t!=false){x=x[2]}else{if(typeof x[3]=='undefined'){return null}else{x=x[3]}}} else if(x[0]=='q'){return x[1]}else{var
f=E(x[0],n)
if(typeof f=='function'){var e=G(x,n)
return e.shift().apply(null,e)}else if((f.length>1)&&(f.length<4)){
var m=f.length==3
var v=0
var b=m?f[1]:f[0]
if(!Array.isArray(b)){b=[b]
v=1}
var h=m?f[2]:f[1]
p=x.slice()
p.shift()
var g=m?p:G(p,n)
if(v){g=[g]}x=h
n=new D(F,b,g)}}}}else{return G(x,n)}}}
H=a=>a==null?'nil':Array.isArray(a)?'('+a.map(H).join` `+')':typeof a=='function'?'<function>':a  
P=x=>H(x)
X=x=>{try{R('('+x+')').map(x=>{console.log(P(E(x,F)))})}catch(e){console.log(e)}}

Try it online!

In this golfed version, the main function that executes code is called X. X takes a single argument - the tinylisp program - executes it and returns the results of execution.

The test cases use their own execute function which in turn uses R, E, P functions (Read, Evaluate, Print). The footer in the TIO link includes the test cases from the question. Expected output is "Success!".

The following is an ungolfed version:

  function READ(x){ 
  
    const tokenize = function(input) {
      return input.replace(/\(/g, ' ( ')
                  .replace(/\)/g, ' ) ')
                  .trim()
                  .split(/\s+/);
    }
     
    const categorize = function(input) {
      if (input.match(/^\d+$/)) {
        return parseInt(input);
      } else if(input == 'nil') {
        return null ;
      } else {
        return input ;
      }
    }
  
    const parenthesize = function(input, list) {
      if (list === undefined) {
        return parenthesize(input, []);
      } else {
        var token = input.shift();
        if (token === undefined) {
          return list.pop();
        } else if (token === "(") {
          list.push(parenthesize(input, []));
          return parenthesize(input, list);
        } else if (token === ")") {
          return list;
        } else {
          return parenthesize(input, list.concat(categorize(token)));
        }
      }
    }
  
    return parenthesize(tokenize(x)) 
  }
  
  function Env(outer, binds, exprs){
    
    if(typeof binds == 'undefined'){ 
      binds = []
    }
    
    if(typeof exprs == 'undefined'){ 
      exprs = []
    }
    
    this.outer = outer
    
    this.data = {}
    
    for(var i = 0; i < binds.length; i++){ 
      this.set(binds[i], exprs[i])
    }  
  }
  
  Env.prototype.set = function(symbol, value){ 
    this.data[symbol] = value
  }
  
  Env.prototype.find = function(symbol){ 
    if(symbol in this.data){ 
      return this
    }
    if(this.outer != null){ 
      return this.outer.find(symbol)  
    }
    return null  
  }
  
  Env.prototype.get = function(symbol){ 
    var env = this.find(symbol)
    if(env == null)
      throw "not found: " + symbol
    return env.data[symbol]  
  }
  
  var repl_env = new Env(null)
  
  repl_env.set('c', (a, b) =>{r=b?b.slice():[]; r.unshift(a); return r })    
  repl_env.set('h', (a) => a?a.length?a[0]:null:null)    
  repl_env.set('t', (a) => a?a.length>1?a.slice(1):null:null)    
  repl_env.set('s', (a, b) => a - b)    
  repl_env.set('l', (a, b) => a < b ? 1 : 0)
  repl_env.set('e', (a, b) => { 
    eq=(a,b)=>{ 
      if(Array.isArray(a)){ 
        if(a.length!=b.length) return 0
        e=1
        for(i=0;i<a.length;i++){ 
          e=e&&eq(a[i],b[i])    
        }
        return e 
      }
      else        
        return a==b ? 1 : 0
    }    
    return eq(a, b)
  })    
  
  function EVAL(x, env){ 
    
    function eval_ast(ast, env){ 

      if(Array.isArray(ast)){ 
        return ast.map(k => EVAL(k, env))
      } else if(typeof ast == 'string'){ 
        return env.get(ast)
      } else { 
        return ast  
      }
    }

  while(true){   
    
    if(Array.isArray(x)){ 
      if(x.length == 0){ 
        return x
      } else { 
 
          if(x[0] == 'v'){ 
            x = EVAL(x[1], env) 

          } else if(x[0] == 'd'){ 
            var symbol = x[1]
            var value = EVAL(x[2], env) 
            env.set(symbol, value)
            return symbol

          } else if(x[0] == 'i'){ 
            var test = EVAL(x[1], env)
            if(test != null && test != false){ 
              x = x[2]
            } else { 
              if(typeof x[3] == 'undefined'){ 
                return null
              } else { 
                x = x[3]
              }
            } 
    

          } else if(x[0] == 'q'){ 
            return x[1]
        
        } else { 
          var first = EVAL(x[0],env)
          
          if(typeof first == 'function'){ 
            var e = eval_ast(x, env)
            var fn = e.shift()
            return fn.apply(null, e)
          }

          else if((first.length > 1)&&(first.length < 4 )){ 

            var m = first.length == 3

            var variablearity=false 

            var binds = m ? first[1] : first[0]

            if(!Array.isArray(binds)){ 
              binds=[binds]
              variablearity=true 
            }

            var body = m ? first[2] : first[1]

            p = x.slice()
            p.shift()

            var args = m ? p : eval_ast(p, env)
            if(variablearity){ 
              args=[args]
            }

            var newEnv= new Env(repl_env, binds, args)

            x = body
            env = newEnv
            
          }
        } 
      }
    } else { 
      return eval_ast(x, env)
    }
  }

  }
  
  function PRINT(x){ 
  
    function stringify(a){ 
      if(a == null){ 
        return 'nil'
      } else if(Array.isArray(a)){ 
        return '(' + a.map(stringify).join(' ') + ')'
      } else if(typeof a == 'function') { 
        return '<function>'
      } else { 
        return a
      }   
    }  
  
    return stringify(x)
  }
  
  function exec(x){ 
    try{ 
      READ('('+x+')')
        .map(x=>{ 
          console.log(PRINT(EVAL(x, repl_env)))
        })
    }catch(e){ 
      console.log(e)
    }  
  }

Approach

The READ function is mostly copied from Mary Rose Cook's Little Lisp Interpreter. For the rest of the code, my starting point was a JS LISP interpreter that I had already built by following Make A Lisp. I had to do quite a few modifications to get it to work with the specified constraints however.

First, I had used JS anonymous function for user defined functions. I had to change that since in tinylisp we only know if there is a user defined function by inspecting the length of the list at the first position in a list. Also, I had used functions for my special forms, but that had to change to get TCO to work. In the end EVAL is one long function which contains most of the tinylisp workings except for built-in functions which remained in their own JS functions.

When golfing my solution, I replaced all variable names with single-letter variable names. I also moved a few functions out to global scope, which enabled further shortenings. If you inspect the code you will see several occurrences of the var keyword. I haven't checked all of these are necessary, but I believe most of them are. Even with TCO in place EVAL still calls itself in some places, and so some variables need to be defined locally and not globally.

In the code there is one class (prototype based), which means in turn that I have a few places where I've used function keyword rather than ES6 arrow functions. Recall that ES6 arrow functions don't define this, and hence can't be used for class methods.

Update

After thinking about it some more, I realized there is a flaw in my submission. Since this is LISP, the built-ins c, h and t should all have O(1) time complexity. What I had done is to implement S-Expressions as Arrays, and so needed to slice() my S-expression when I wanted to get the tail.

We can test this with the following tinylisp code:

(d K 1000000)

(d f 
(q((x y)
  (i
    x
    (f (s x 1) (c 1 y))  
    y
  )
)))

(d M (f K (q(1))))

(d g 
(q((x y)
  (i
    x
    (g (s x 1) (c (t M) y))  
    y
  )
)))

(d S
(q ((x)
  ()
)))

(S (g K ()) )


Try it online!

In the code, I create an S-expression with 1,000,000 elements, and then try to call t on it 1,000,000 times. It runs in about 50 seconds on TIO. This suggests to me that the reference implementation is doing things properly :)

With my JavaScript code however, I get out-of-memory error. Why? Because I am trying to create 1,000,000 copies of a 1,000,000 element expression.

So, I re-wrote my code. I include the un-golfed version here for you to look at (If I have time, I will update my golfed version too). It runs the test code in about 5 seconds on TIO.

/*
 In this version, we first test the implementation to see how fast we can perform c/h/t operations.
*/

const toSExpr = function(arr) { 
    if(Array.isArray(arr)){ 
      if(arr.length == 0){ 
        return null 
      }
      else { 
        return [ toSExpr( arr[0] ), toSExpr(arr.slice(1))]
      }
    }
    else { 
      return arr 
    }
  }

// I think this is what we need in eval_ast  
const toSExpr2 = function(arr) { 
    if(Array.isArray(arr)){ 
      if(arr.length == 0){ 
        return null 
      }
      else { 
        return [ arr[0], toSExpr2(arr.slice(1))]
      }
    }
    else { 
      return arr 
    }
  }


function READ(x){ 
  
    const tokenize = function(input) {
      return input.replace(/\(/g, ' ( ')
                  .replace(/\)/g, ' ) ')
                  .trim()
                  .split(/\s+/);
    }
     
    const categorize = function(input) {
      if (input.match(/^\d+$/)) {
        return parseInt(input);
      } else if(input == 'nil') {
        return null ;
      } else {
        return input ;
      }
    }
  
    const parenthesize = function(input, list) {
      if (list === undefined) {
        return parenthesize(input, []);
      } else {
        var token = input.shift();
        if (token === undefined) {
          return list.pop();
        } else if (token === "(") {
          list.push(parenthesize(input, []));
          return parenthesize(input, list);
        } else if (token === ")") {
          return list;
        } else {
          return parenthesize(input, list.concat(categorize(token)));
        }
      }
    }

    var sExpr = toSExpr( parenthesize(tokenize(x)) )
     
    return sExpr
  }
  
  function Env(outer, binds, exprs){
    
    if(typeof binds == 'undefined'){ 
      binds = []
    }
    
    if(typeof exprs == 'undefined'){ 
      exprs = []
    }
    
    this.outer = outer
    
    this.data = {}
    
    for(var i = 0; i < binds.length; i++){ 
      this.set(binds[i], exprs[i])
    }  
  }
  
  Env.prototype.set = function(symbol, value){ 
    this.data[symbol] = value
  }
  
  Env.prototype.find = function(symbol){ 
    if(symbol in this.data){ 
      return this
    }
    if(this.outer != null){ 
      return this.outer.find(symbol)  
    }
    return null  
  }
  
  Env.prototype.get = function(symbol){ 
    var env = this.find(symbol)
    if(env == null)
      throw "not found: " + symbol
    return env.data[symbol]  
  }
  
  var repl_env = new Env(null)
  
  repl_env.set('c', (a, b) =>[a,b])    
  repl_env.set('h', (a) => a?a.length?a[0]:null:null)    
  repl_env.set('t', (a) => a?a.length?a[1]:null:null)   
  repl_env.set('s', (a, b) => a - b)    
  repl_env.set('l', (a, b) => a < b ? 1 : 0)
  repl_env.set('e', (a, b) => { 
    eq=(a,b)=>{ 
      if(Array.isArray(a) && Array.isArray(b)){ 
        return eq(a[0], b[0]) && eq(a[1], b[1])
      }
      else if(Array.isArray(a) || Array.isArray(b)){ 
        return 0
      }
      else {
        return a==b ? 1 : 0
      } 
    }     
    return eq(a, b)
  })    

  function lenSExpr(sExpr){ 
    if(sExpr)
      return 1 + lenSExpr(sExpr[1])  
    else 
      return 0
  }

  //translate sExpr to equivalent Array
  //only one level deep 
  function toArray(s){
    var ret = []
    while(s != null){ 
      ret.push(s[0])
      s = s[1]    
    }
    return ret 
  }

  function EVAL(x, env){ 
    
    function eval_ast(ast, env){ 
      //should take an sExpr and return an SExpr
      if(Array.isArray(ast)){ 
        var t = toArray(ast)
        var results = t.map(k => EVAL(k, env))        

        return toSExpr2(results) 

      } else if(typeof ast == 'string'){ 
        var ret = env.get(ast)
        return ret 
      } else { 
        return ast  
      }
    }

  while(true){   

    if(Array.isArray(x)){ 
      if(x.length == 0){ //not used anymore x.length can't be zero
        return x 
      } else { 
 
          if(x[0] == 'v'){ 
            x = EVAL(x[1][0], env) 

          } else if(x[0] == 'd'){ 
            var symbol = x[1][0]
            var value = EVAL(x[1][1][0], env) 
            env.set(symbol, value)
            return symbol

          } else if(x[0] == 'i'){ 
            var test = EVAL(x[1][0], env)
            if(test != null && test != false){ 
              x = x[1][1][0]
            } else { 
              if(typeof x[1][1][1] == null){ 
                return null
              } else { 
                x = x[1][1][1][0]
              }
            } 
          } else if(x[0] == 'q'){ 
            return x[1][0]
        
        } else { 
          var first = EVAL(x[0],env)
          
          if(typeof first == 'function'){ 
            var e = toArray(eval_ast(x, env))
            
            var fn = e.shift()

            return fn.apply(null, e)
          }

          else if((lenSExpr(first) > 1)&&(lenSExpr(first) < 4 )){ 

            var m = lenSExpr(first) == 3

            var variablearity=false 

            var binds = m ? first[1][0] : first[0]


            if(!Array.isArray(binds)){ 
              binds=[binds]
              variablearity=true 
            }
            else{
              binds=toArray(binds)
            }

            var body = m ? first[1][1][0] : first[1][0]

            p = x[1]

            var args = m ? p : eval_ast(p, env)


            if(variablearity){ 
              args=[args]
            }
 
            var newEnv= new Env(repl_env, binds, toArray(args))

            x = body
            env = newEnv
            
          }
        } 
      }
    } else { 
      return eval_ast(x, env)
    }
  }

  }
  
  function PRINT(x){ 
  
    function stringify(a){ 
      if(a == null){ 
        return 'nil'
      } else if(Array.isArray(a)){ 
        return '(' + toArray(a).map(stringify).join(' ') + ')'
      } else if(typeof a == 'function') { 
        return '<function>'
      } else { 
        return a
      }   
    }  
  
    return stringify(x)
  }
  
  function exec(x){ 
    try{ 
      toArray(READ('('+x+')'))
        .map(x=>{ 
          console.log(PRINT(EVAL(x, repl_env)))
        })
    }catch(e){ 
      console.log(e)
    }  
  }

//------------------------------------------------------------------------------------------------------------

function do_test(t, e){ 
    function test_exec(x){ 
        return toArray(READ('('+x+')'))
            .map(y=>{ 
              //console.log('y', y)   
              return PRINT(EVAL(y, repl_env))
            }).join('\n')
   }

   actual = test_exec(t)

   if(actual == e.split('\n').map(x=>x.trim()).join('\n')){ 
     console.log('Success!')
   }
   else{ 
    console.log('Fail!')
    console.log(actual)
   }
 }


tests = `

(d K 1000000)

(d f 
(q((x y)
  (i
    x
    (f (s x 1) (c 1 y))  
    y
  )
)))

(d M (f K (q(1))))

(d g 
(q((x y)
  (i
    x
    (g (s x 1) (c (t M) y))  
    y
  )
)))

(d S
(q ((x)
  ()
)))

(S (g K ()) )

`


expected = `K
f
M
g
S
nil`

do_test(tests, expected)


Try it online!

\$\endgroup\$
2
  • \$\begingroup\$ Nice! I did notice one problem and one golfing opportunity. The problem: nil is not a reserved name, so a test case like (d nil 42) nil should output nil 42, whereas your solution outputs nil nil. The golf: replace(/\(/g,' ( ').replace(/\)/g,' ) ') can become replace(/[()]/g,' $& '). \$\endgroup\$
    – DLosc
    Commented Aug 16 at 21:25
  • \$\begingroup\$ The reference implementation doesn't use true linked lists (example) and anyway it's not a requirement in the challenge to handle that. I'm guessing where your solution goes wrong is, for whatever reason, not having the subarrays be garbage collected (or possibly python does some immutable/copy-on-write slice trickery), but it doesn't matter anyway. \$\endgroup\$
    – emanresu A
    Commented Aug 31 at 9:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.