Wisp: A light Lisp written in C++
github.com
github.com
Not one person cares that HN-LISPER is not impressed that OP wrote yet-another-Lisp-interpreter-in-language-X or that you're unhappy with the style of their parentheses. Save it.
As "easy" an exercise as it may be, I will guarantee you that OP learned a lot about Lisp, the host language, its build infrastructure, etc. The project is currently unlicensed (you should think about licensing it, OP!), but the source is available for others to study and learn from, too.
So, I say bravo!
tldr; Go ahead and use S-Expression syntax but don't put "lisp" in the name. Rich Hickey knew this when he made Clojure.
Almost everybody has their own lisp implementation. Some programmers' dogs and cats probably have their own lisp implementations as well. This is great, but too often I see people omit some of the obscure but critical features that make lisp uniquely wonderful. These include read macros like #. and backreferences, gensyms, and properly escaped symbol names. If you're going to waste everybody's time with yet another lisp, at least do it right damnit.
For example, I would personally not consider macros (let alone reader macros or gensym) to be something that a language must have in order to be called a lisp. Such an opinion implies that the first true lisp was not introduced until a 15 years after the first language that called itself lisp, and tacitly casts McCarthy himself as one of those hapless poseurs who tried to pass an unconvincing lisp wannabe as the real deal.
(At least according to the timeline given in Steele and Gabriel's history of lisp. https://dreamsongs.com/Files/HOPL2-Uncut.pdf)
Getting twitchy about the syntax also seems somewhat odd to me, considering that Lisp 1.5 did things like
cons (x (y))
=> (x y)Smaller than femtolisp
(1) C++ has std::to_string nowadays. You don't need to roll your own.
(2) Environment::get would be more efficient if you used a hash table (e.g. unordered_map) instead of many ifs.
(3) It's better to pass std::string and std::vector by const reference, to avoid making a copy. Also take a look at std::string_view.
(4) const char[] is generally preferable to #define.
Go to any open source project hosting site, and perform a search for the term Lisp Interpreter. You'll likely get a cyclopean mountain of results from this seemingly innocuous term.
Here's the thread which has the barebones overview which inspired me:
https://news.ycombinator.com/item?id=13082825
I could have taken it further, but the implementation there is not "real" in the sense that there is no real return-stack, so you can't implement IF-statements using the lower-level primitives.
That said it is a good starting point, and I had some fun doing it. I'd guesstimate it is more of a single weekend project though, rather than longer.
http://dustycloud.org/blog/wisp-lisp-alternative/
and a little Clojure-like LISP in JavaScript:
Please people, check your names before creating more confusion. Sometimes it is OK to use the same name, but here it will only lead to confusion.
Though, the code is pretty clear and easy to follow.
There's certain amount of "eye rolling" that greets these "I wrote a Lisp interpreter" articles, because 1. Lisp interpreters are incredibly simple and uninteresting, and 2. Lisp compilers have been around for more than 60 years now http://bitsavers.org/pdf/mit/rle_lisp/LISP_I_Programmers_Man....
It would be more interesting if you wrote a Lisp compiler in C++ which included a REPL, or even compiled Lisp to C++.
Also, a Lisp user would never write code like this:
))
))
Lastly, seeing Lisp interpreters still being blabbed about perpetuates the untrue meme that Lisp is inherently slow, or ugh interpreted. Which is sad.Call me crazy, but how does the language you do this in become relevant? As a reference perhaps? Only the best of intentions here, have i missed a paticular pov that makes this extra relevant?
* For projects where one of the main purposes is for the author of the project to learn said language
* For projects which expose an API in their implementation language. In the case of an implementation of a programming language, they might offer embeddability in the host language, or a foreign function interface with the host language.
Is there any evidence that the "user" of an API is likely to prefer the same language that was used for the original thing?
(BTW this does not seem to be the case for networked APIs such as REST services).
Direct FFIs are possible, but generally don't happen except in exceptional cases because of the cost involved in writing a new FFI (and accompanying set of gotchas) for every possible language whose packages you might want to consume. The best and worst thing about the C ABI is that it's pretty much the least common denominator.
Even in platforms like Java and .NET that theoretically smooth over these problems, we find that it never works out that well in practice. Scala and F# users end up having to write compatibility layers in order to preset Java and C# friendly APIs, and they end up writing their own versions of just about every possible library in order to have packages that don't feel awkward to use in their language of choice. The only real exception I know of to that pattern on the JVM or the CLR is between C# and Visual Basic, and that is only because Microsoft was very careful to ensure that the two languages' semantics were similar enough to virtually guarantee no cross-language friction.
I'd suggest that in many forums it is irrelevant to most and should be left off, but I suspect HN is not a representative sample of the general public and many may share my sentiment.
So even if I cannot read the code, I can mentally get an idea that tool X is a good fit for problem Y should it ever come up for me.
Nope, language flamewars were pretty much alive on BBSs and USENET.
I have had my share against C on comp.lang.c and comp.lang.c.moderated.
Interpreting a simple lisp-like in lisp is borderline trivial. Interpreting a simple lisp-like in an imperative language demonstrates a significantly more fundamental understanding of how lisp-like languages work.
As far as I am concerned there is one aspect where I think it is relevant for me, as I compile all software that runs on my machine. If it written in C or in C++, there are good chances that I won't need to compile an horrible chain of dependencies (or most likely, try and fail at compile that chain).
That matters to me, because I try a lot of alternative/niche/toy languages, and if I consider those published within, say, the last ten years, half of them won't even compile. It is a disaster. If it is written in shell + C, I can fix a couple errors easily (if there are more I give up because there is no point in wasting my time on something that claims to be next programming marvel and yet is itself programmed like shit). People want to be fancy and pick up toolchains that are not battle-tested like Autotools+C or CMake+C++, and that's the recipe for pain.
For example, today I tried to compile two languages written in Ada, and I had to give up. A mess. Each time I tried to build an Ada project which wasn't mine, it has been horrible. That's antinomic to the image of Ada (clean and strict to the point of being stiff) but that's my repeated experience with building.
I am not even talking about the anti-feature which is compilers which are written in the very language they aim to compile and do not even provide a bootstrapping minimal compiler written in C or similarly fundamental and widespread language. They're generally very proud of their accomplishment :-/
So yes, a project in C (or C++) offers the best chances of success (for building, which is the point I developed here).
Also, considering toy language, I often want to make a dirty hack to add a missing features. If it is written, say, in Haskell, there is zero chance I can do it. Good old imperative languages like C, Python, Pascal, I have a chance and I have already done it.
There is also the case of all the languages built over the JVM. There are really many. But even when they look good to me, they are limited by the opinionated choices of the JVM, and I don't wanna undergo the multiple programming neuroses of Gosling. Also anything Java based has always been terribly, awfully, painfully, excruciatingly slow on any of my machines; I suppose it not an universal experience, but it has always been mine.
------
> compared to well tested, design, architectured, community engagement, track record, performance and usability.
To each is own: I care about well tested, usability, and depending on the intended use, performance. I also care a lot about proper exhaustive documentation, which is missing from your list and from most programming languages projects.
But I don't give a damn about its design and architecture, what matters to me is the external features, not the internals. Language designers care way too much about the internals and neglect the outside, which is what matters to the end user. I don't care if it make your parser more complicated, it if makes your grammar irregular, if you need more passes, if you need to dirty your hands with context; I want that source code using your language flows, for writing and for reading.
Community? I don't really care, and I have seen it being detrimental to many projects, which lose their compass by wanting to incorporate every feature request and following the mood of the year; I'd rather they were developed between closed doors and only published when they reach beta status, to maintain consistency, instead of jumping from an unpolished feature to the next. I am aware that's totally against the current trend, where people publish repositories with a README.md before writing the first line of code or documentation :-)
>To each is own: I care about well tested, usability, and depending on the intended use, performance. I also care a lot about proper exhaustive documentation, which is missing from your list and from most programming languages projects.
~But I don't give a damn about its design and architecture, what matters to me is the external features, not the internals. Language designers care way too much about the internals and neglect the outside, which is what matters to the end user. I don't care if it make your parser more complicated, it if makes your grammar irregular, if you need more passes, if you need to dirty your hands with context; I want that source code using your language flows, for writing and for reading.
I think there's a disconnect here between good design and architecture and the result of it. You can't by definition have a good design and architecture that makes your program difficult to use and illogical externally. You can't have good tests without a good design and architecture. Good design and architecture goes hand in hand with usability too.
To go into a deep dive is beyond the scope here. But concisely said, good designs and architectures makes the software easy to understand and easy to change to external changes of requirements. Since its easy to reason about it's also easy to understand and reason about bugs when they inevitably shows up. If the software does not have these features, it does not have a good designs and architecture. You might think a certain architect cares more about the internals then the external, but then the architect is bad, and should study and should step back a step and think about the end user and heading of the software project. Or perhaps there's a communication issue going on, human <-> human style.
Good designs and architectures also is the equivalent, actually superior to, having good documentation. Documentation that defines the external goals are not required if you have good tests that shows them. Hell documentation about the code is not needed if the design and architecture is good. And the self documenting code can never lie. Is it difficult to achieve? Yes. But not trying will put you into a corner you can't get out of and inevitably lead to the rewrite-from-scratch. Unless of course you have a very strictly defined end goal and complete definition of done, but you could still paint yourself into a very difficult corner to get out of.
>Community? I don't really care, and I have seen it being detrimental to many projects, which lose their compass by wanting to incorporate every feature request and following the mood of the year; I'd rather they were developed between closed doors and only published when they reach beta status, to maintain consistency, instead of jumping from an unpolished feature to the next. I am aware that's totally against the current trend, where people publish repositories with a README.md before writing the first line of code or documentation :-)
Not sure i see the correlation between pushing readmes with intent and publishing unfinished feature upon unfinished feature. Isn't readme:s infact documentation? Well well :p i believe you can work in the open without jumping to unfinished feature to unfinished feature. Polish can come in the middle of a development cycle, not just the end. A vibrant community around a software also is equivalent with many contributors etc, which makes the software resilient to people moving to other things etc.
I might have sounded critical, but enjoyed reading your response. Thank you for your response :) I also believe you gave me clues to why the "written in" header is so popular.
The way you interpret certain values i gave was also insightful. I certainly don't have the same opinions, but non the less i can see where you come from. So i just had to respond to it :) Thank you!
Quoting means that a thing is returned as itself and not evaluated. Thus it is not interpreted in any other way.
Take IF, it does not quote its arguments, it just conditionally evaluates them based on the result of the test form. It implements control flow. Thus IF evaluates things - it does not quote them - they are not returned as they were in the source code.
(if
test-form ; evaluated first
then-form ; evaluated when test-form is true
else-form) ; evaluated when test-form is false
Thus each of the above three argument forms might be evaluated, but only one of the last two, depending on the evaluation of the first form. In a compiled Lisp, the then-form and else-form will not be stored as quoted forms in the code, they will be compiled to some other language (often machine language, C, or similar), just as the rest of the source code. Thus after compilation all these source forms will disappear and only executable code will be left. Thus there is nothing to quote by the IF operator.Special operators usually the core of needed built-in control flow and scoping operators. Typically in Lisp, they are also not user extensible in the standard language: the user can't write new special operators.
Many other additional operators are derived from those via macros, which transform source code to new source code which eventually only contains function calls and special forms.
It probably won't be very fast though. There are parts of C++ that doesn't map very well to common lisp, and which means some parts will have more overhead than necessary.
> He had the wonderful idea of not implementing any undefined behaviour that wasn't strictly necessary
What does this mean? It sounds like the way an ordinary C compiler works: it ignores the possibility of undefined behaviour occurring, as the C standard allows it to do this.
If you had signed integer over/underflow it wrote insulting error messages.
http://www.hanno.jp/gotom/Cint.html
https://github.com/root-project/cling
https://www.softintegration.com
You can argue whether some of those are strictly interpreters, versus just a REPL hooked up to a compiler (as in the case of Cling). But they do exist.
p.s. here's the most concise two refs i found by googling.
https://www.quora.com/What-is-the-purpose-of-dlopen-dlload-i...
"...I decided to implement a Java Virtual Machine in Common Lisp - under normal circumstances, I wouldn't have dared to do this, because this is quite a complex undertaking, but I had read in several places that Lisp would be suitable for projects that you would normally not dare to do otherwise, so I thought I would give it a try. Over the course of 8 weeks, with something like 2 hours per day, or so (because I was still doing other stuff during the day), I was able to get a first prototype that would execute a simple "Hello, World!" program. On top of that, it was a portable (!) just-in-time compiler: It loaded the bytecode from a classfile, translated it into s-expressions that resemble the bytecodes, and then just called Common Lisp's compile function to compile those s-expressions, relying on macro and function definitions for realizing these "bytecodes as s-expressions." I was really impressed that this was all so easy to do.
The real moment of revelation was this: to make sure to reuse as many of the built-in Common Lisp features as possible, I actually translated Java classes into CLOS classes, and Java methods into CLOS methods. Java's super calls posed a problem, because it was not straightforward to handle super calls with plain call-next-method calls. Then I discovered user-defined method combinations, which seemed like the right way to solve this issue, but I was still stuck for a while. Until I discovered that moving a backquote and a corresponding unquote around actually finally fixed everything. That was a true Eureka moment: In every other programming language that I am aware of, all the problems I encountered until that stage would have required a dozen redesigns, and several attempts to start the implementation completely from scratch, until I would have found the right way to get everything in the right places."
[0] - lisp-univ-etc.blogspot.com/2012/04/lisp-hackers-pascal-costanza.html
I'm not exactly sure what you mean with just "static", I'm assuming static types. But wouldn't a language with static types and a repl not be as powerful as if it was dynamic instead? In the end, you want to be able to redefine everything via the repl in your runtime, and if there is types to be checked all the time and the compiler has to do extra steps for each eval, it'll get in your way and make it harder to monkey-patch things.
A c/cpp repl would make people fiddle around and have a rapid intuition. Then they go without.
Cling- https://root.cern/cling/ (and the earlier Cint- http://www.hanno.jp/gotom/Cint.html)
I used to occasionally use them for local experimenting with libraries and language features.
A cycle that went something like:
1. Write the line to a file
2. Compile file with default flags
3. Run binary in gdb causing gdb to dump certain interest info (stack info, variable contents, etc)
4. Read the sections/symbols and merge this data into the gdb data (maybe get it from gdb)
5. Dump this data into your reply and refresh waiting for user input.
You could have a few basic commands in the repl:Modify the compiler args Add/remove/edit a line Attach gdb.
Emacs or vim could easily do this, and I suspect there are people who already do almost exactly this.
You could also just use any editor/ide with debug integration and keep a scratch file around...
We're talking about a repl here, not just a basic shell. You'll need to be able to redefine functions and everything else at runtime so a repl is all you need to run your program. A lisp with a standard repl is running miles around any editor/ide with debugger support, as the workflow with repls, especially ones that can nest on exception and so on, is much faster.
https://news.ycombinator.com/item?id=25531871
https://github.com/vsedach/Vacietis
================
Vacietis is a C compiler for Common Lisp systems.
Vacietis works by loading C code into a Common Lisp runtime as though it were Lisp code, where it can then be compiled or evaled. The loaded C code has the same function calling convention as regular CL code and uses the same numerical representations. C memory is backed by regular Common Lisp arrays.
Vacietis comes with a libc implemented in portable Common Lisp.