Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
fe: A tiny, embeddable language implemented in ANSI C (github.com/rxi)
177 points by sph on June 8, 2023 | hide | past | favorite | 106 comments


When I see amazingly compact language implementations like this, the effect on me is not 'I want to use this language' but rather 'I should also write my own LISP'.


I misread it as 'I should write my own LSP [for that language]', which might be neat project too!


I wrote a lisp, a while back, and then later added an LSP for it.

Since lisp is so simple in terms of syntax what I really did was tab-completion, and info-on-hover, for the built-in functions like "car", "cdr", and the primitives I added as part of a more complex standard-library.

In my case I was writing in go and I found an LSP-server package which was trivial to use. So getting the integration with emacs, vim, etc, was really trivial:

https://github.com/tliron/glsp


Strange effect however I kinda felt it too. But alas `#include <setjmp.h>` here be dragons.


Unless you want half your C logic to be error propagation, it’ll have to be <setjmp.h>, all its flaws notwithstanding. Manual unwinding can be done tastefully (see Tcl), but in the single-digit-KLOC weight class it’s not really a choice.


while we're marvelling about how small and compact it is, then just replace them (with something uglier and less performant), it's not rocket science


> Donnie: I made a new language

> Therapist: New language, or LISP dialect?

> Donnie: LISP again

Mostly a dig at the title. Jokes aside, I love how concise this is. Not sure how I feel about readability, too many inline if {} blocks for my liking, and a bit arcane. But I'm never not gonna upvote novel languages. Something about language creation delights me.


If this project interests you but is a bit more minimal than you need, the Janet language is a slightly-less but still pretty lightweight embeddable Lisp with a strong library and community: https://janet-lang.org/


Has anyone here switched from Common Lisp or Scheme/Racket to Janet? If so, what was good/bad about the switch? Or was it just different?

I recently started learning Common Lisp, and it’s been mostly great, but my two complaints are 1) memory usage and 2) deployment (copy source or distribute a relatively huge world image—and I haven’t found any way to get a fully static build without patches, even with ECL).

Janet could solve both of those, although I’d pay a performance penalty due to it being interpreted, and I assume there are a lot fewer libraries/tools. I really like the idea of a Lua-sized Lisp, but I’m not sure if I should jump ship from a traditional Lisp yet.


I’ve never done basically any lisp except a toy project but this

> distribute a relatively huge world image

in my understanding of the lisp world is actually a feature…?


You’re right, it is a feature, but I’d like the option to distribute a smaller binary without everything built in. Roswell supposedly helps with this, but I haven’t gotten it to work yet. Some commercial implementations support this, I think, but they’re priced well beyond my hobby project budget.

Janet is intriguing to me because it has a small footprint (even smaller than most Schemes). I’ve heard it’s handy for building little utilities, especially if you want someone else to be able to run them (a situation I find myself in regularly).


To provide a some extra context for other readers and mostly respond to the GP:

Janet does not do any stripping for distribution -- Janet images compiled to native executables still contain the full compiler/stdlib/runtime/everything in <1mb.


Janet is planned and built as an embeddable language; CL is not.


Absolutely, but it’s not even embedding that I had in mind. I want to be able to produce fully statically linked binaries, because in my experience that is the only truly portable binary solution on Linux. It’s a trade off for sure, but it’s something I’ve found useful for running on e.g. Raspberry Pi.

For SBCL, the two main approaches I’ve seen are: a) use a patched version of SBCL that is statically linked or b) build against an old glibc and hope for the best (which sadly doesn’t work on my musl libc-based distribution). I guess the third approach is distribute source and compile on the device—this is what I’m actually doing, but it seems a little silly to recompile every time, even if it’s the same architecture.

And keep in mind this is just my wishlist of functionality. It’s not necessary and I don’t expect it, but it would be really convenient.


easiest solution I found was building SBCL targetting an old glibc using zig as the c compiler:

CC="zig cc -target x86_64-linux-gnu.2.28" LD="zig cc -target x86_64-linux-gnu.2.28"

zig bundles in all the glibc headers, so I think it should build on musl libc-based distro

for more complicated programs with native dependencies there is (https://github.com/Shinmera/deploy) or guix has a lot of CL libraries already and it's fairly easy to write packages for others, you can specify all dependencies with guix and use guix pack to get a .tar with all dependencies you can unpack and run on any other linux box


I was wondering if compiling via Zig would work. Also, thanks for the Guix tip—I will give that a try!

I think native dependencies would require something like this (which I believe uses Deploy to solve part of the problem): https://www.timmons.dev/posts/static-executables-with-sbcl.h...


I just started toying with Janet recently & so far it’s very pleasant


Does Janet have something like Clojure's threading (->, ->>)?


For a Lisp not to have threading macros, it would need not to have macros, period. Writing these is so trivially simple you can ask ChatGPT to do it and it would get it right on the first try.

On a related note: it would be an interesting read if someone tried tracing the threading macros to some origin point(s) in the past. There seem to be two main variations, Lisp's ->/->> and |> in many functional languages (Elixir, OCaml, Raku). The construct seems to have gotten popularized by Clojure and F#, but I don't think it was invented in either (maybe rediscovered).

It had to start at some point, somewhere, but I wasn't able to locate that point.



I was wondering what the goal of the project is. The README is not very clear on it, but the implementation document [1] does state design goals:

- Small memory usage within a fixed-sized memory region — no mallocs

- Practical for small scripts (extension scripts, config files)

- Concise source — less than 1000 loc

- Portable ANSI C (Windows, Linux, DOS — 32 and 64bit)

- Simple and easy to understand source

- Simple and easy to use C API

[1] https://github.com/rxi/fe/blob/master/doc/impl.md


> - Portable ANSI C (Windows, Linux, DOS — 32 and 64bit)

I skimmed through the source, and aside from reading a file from STDIN to a `static char buf[64000];`, nothing in this seems to use the POSIX API. With that buffer trimmed to an appropriate length, it appears it could run on a microcontroller, which is always a useful thing to have.


The author developed some games. Maybe the goal of fe is to write game scripts.


I once wrote a tiny DSL language. I used LISP (S-Expressions with parenthesis) because it semantically fit and made the parser much easier to write. I was laughed at and ridiculed by my peers. Everyone hated it that I had to attempt two other language styles just to compensate. I had a chuckle seeing fe syntax is LISP like and I was like “See? I’m not the only one.” Many just can’t give LISP and S-Expressions their due regard. Same for Reverse Polish Notation. I was born on the wrong planet.


John McCarthy had the opposite experience: he originally planned to replace Lisp's S-expressions with a more ALGOL-like syntax called M-expressions, but other people liked S-expressions and he never got around to implementing M-expressions.

https://en.wikipedia.org/wiki/M-expression


I love RPN for calculating - I still regularly use a HP11C (Swiss Micros replica).

But I hate coding or reading prefix (list) or postfix (rpn, forth, etc) languages. Once wrote a substantial project is Postscript - works fine but note easy on the brain.

Guess it comes from just too much familiarity with infix notation!


I personally find prefix and RP notations to be pretty easy to read, but writing is harder for me. I've introduced bugs into FORTH programs because I did a + b instead of a b + out of habit. I think both notations have their place and not everyone will be able to make the switch


There are Lisp macros that allow writing infix arithmetics. Even fe is said to support macros.


No you weren't, many of us, like myself, have designed small languages over the years.

After a few of these, you stop designing syntax: you just skip the syntax and directly read ASTs instead of whatever syntax you wasted your time on.

My next language is going to be a FORTH of some type .


I don't think it's worth making a FORTH unless you plan to build an optimizing compiler better than what iForth, gForth, or VFX have done, open source and maintain it, or exclusively target microcontrollers. FORTH is a great "language" but the ecosystem is tiny and the tooling is pretty minimal.


Well, yeah, I hear you.

But consider that I didn't make lisp-like languages because I didn't find anything suitable.

There were (and still are) lots of nice Lisp implementations when I made my variations over the years.

It's the same with any language created by a single person: we aren't doing it so that we can use it in production (that's a bonus, if possible).


Fair enough!


I thought part of the reason people use forth is because they don't need or want the dependencies and complexity of a big ecosystem or lots of tooling.


I worked with Joy for some time and came to the conclusion that syntax is a MacGuffin.


You’d be hailed as a genius if you replaced s-expressions with yaml.


Maybe a little bit offtopic, but rxi also made a few Lua / LÖVE 2D libraries that I've been able to put to good use, namely shash [0], lume [1], json.lua [2] and classic [3].

He makes some nice stuff.

---

[0]: https://github.com/rxi/shash

[1]: https://github.com/rxi/lume

[2]: https://github.com/rxi/json.lua

[3]: https://github.com/rxi/classic


Doesn't really count as language design if it's toy Lisp #9368. Language implementation exercise, yes.


Who is counting?


Well I counted 0 references to Lisp or Scheme in the documentation, so it seems to be presented as novel.


When you have methods named `cons` `car` and `cdr` that appear to work in the same way as lisp's methods of the same name it's pretty clear that you're not pretending or attempting to be "novel".

While it _should_ say it's a lisp / scheme, given those it doesn't really _need_ to say that because it's pretty forehead slappingly obvious to anyone who knows what lisp or scheme are.


But then if you call it a Lisp or even mention Lisp, some other wise guy is going to complain that it's not a real Lisp.


Even if you don't mention Lisp, people will assume you were trying to make it a Lisp and complain that you didn't follow Lisp precedent. Examples can be found in this very comment section.


It has cons cells, and lists terminated by a symbol whose name is nil. It's more of a Lisp than Clojure.


Me, I’m counting!


Femtolisp anyone? Use in Julia I believe

https://github.com/JeffBezanson/femtolisp


There's also ulisp (for Arduino projects etc.): http://www.ulisp.com/

This is larger, because there are functions for accessing peripherals, and the core is more standard lispy with 'caadr' et.al., and it has a compacting GC, so images can be saved as a compact blob.


Not to be confused with Fe: https://fe-lang.org/


Also not be confused with ferric oxide: https://www.rust-lang.org/


There's an awful lot of languages with an .fe code file extension, aren't there:

https://ferite.sourceforge.net/


= for defun, really? why not set?

= really is for equality in lisp, not assignment


APL, ML and Pascal too. There is a story that Ken Iverson, the inventor of APL, was passing a terminal at which a Fortran programmer had just typed:

      I = I+1
Ken paused for a moment, muttered “no it doesn’t”, and passed on.


I read (= a b) as unification and slowly pieced together that it was probably assignment. Not as equality as the results of all the expressions starting = were discarded.


Why is it so strange that someone ignored a convention?


Because it's a 50-year old convention, and there doesn't seem to be a compelling reason for ignoring it. The language then uses `is` for equality, including numbers.

It's just weird, especially given they're using old-fashioned car/cdr instead of head/tail.


> Because it's a 50-year old convention

That's a terrible reason to justify... well, anything, really.


It's not just 50 years old, it's widely conformed to for these 50 years.

Why won't you use "&" for "less" and "%" for "greater", instead of sticking to the tired "<" and ">"?


Slavery was widely confirmed for even longer. Tradition, right?

How about instead of using tradition to decide if something is beneficial or not, you just assess it on its value?


> Slavery was widely confirmed for even longer. Tradition, right?

This is an egregious use of the noncentral fallacy.[1]

Conforming to this 50-year tradition makes the language more familiar and easier to learn, so it's reasonable to question why the language chose differently.

[1]: https://www.lesswrong.com/posts/yCWPkLi8wJvewPbEp/the-noncen...


And yet, Clojure, the most used Lisp today, decided to use brackets instead of parentheses for parameter lists, breaking with 50 years of Lisp tradition. How dare they??

But I'm sure you know better than them.


When Lisp first appeared, square brackets were not even standard characters. Maybe McCarthy would use them if he could.

The word "set" is much older, and is universally supported.

But most glaringly, (= a b) has a well-understood and entirely different meaning in most lisps, also since 1950s. It's comparison, not assignment. Breaking a well-set convention is a very different thing than making a new, slightly improved convention.


My original post includes "there doesn't seem to be a compelling reason for ignoring it". I'm not arguing for tradition for tradition's sake.

I don't think using = instead of define/defun/defn has a compelling reason. Clojure's use of [], {}, #(), etc., is compelling to me. I'm not a Lisp purist who thinks everything should be parentheses. Adding that syntax is helpful for the reader.

Innovation is welcome when it's beneficial. = for assignment isn't.


Using [] to style function formal parameters is completely pointless. It literally serves no purpose. It just makes it gratuitously different from other languages (not only Lisps). In most mainstream languages, both formal parameters and actual parameters use parentheses.

There would be a sense to it if square brackets shifted into some alternative semantics: (fn (...) ...) versus (fn [...] ...) doing something usefully different.

If you must use square brackets there, they are just a syntactic quirk that doesn't enable any new semantics.


Everyone who uses Clojure, and even people who don't, but know Lisp, understand the point: since parentheses are used in Lisp to delineate everything, it makes sense to use a different pair of symbols to make parameter lists stand out more.

Maybe saying it's "completely pointless. It literally serves no purpose" is a bit overly dramatic, don't you think?

You could have limited yourself to saying you don't personally like it because it breaks with tradition.


> it makes sense to use a different pair of symbols to make parameter lists stand out more.

Quite simply, no, it doesn't.

> you don't personally like it because it breaks with tradition.

Nope! I don't impersonally like it, because it's a gratuitous inconsistency which doesn't do anything. It's not technically justified in any way.

How does it make sense to have the formal parameter stand out? Why don't we want the function body to stand out? (fn (a b) [+ a b])?

Don't the parameters already stand out by being on their own line (usually)?

   (defn (a b)  ;; <-- sticks out like sore thumb
     ...) 
If you think something deserves to stand out in code, can't you teach your editor to highlight it?

Where are the numbers to back all this "sense"?


Scheme used braces at least a decade or two before clojure even existed.


March 1960, the Lisp I Programmer's Manual:

  ff[x] = [atom[x] -> x; T -> ff[car[x]]]
or

  DEFINE ((
    (FF (LAMBDA (X)
          (COND ((ATOM X) X)
                (T (FF (CAR X))))))
    )) ()
which one or two decades later would be in Lisp:

  (defun ff (x)
    (cond ((atom x) x)
          (t (ff (car x)))))


This is not a Lisp, and never claimed to be one.

This kind of gatekeeping from ANSI Common Lisp purists is tiring. Who cares about convention when one just wants to build their own language, use S-exprs and car/cdr for the sake of it.


> Who cares

Potential users care. It's off-putting to use something familiar but different in strange ways.

Using head/tail versus car/cdr would be a deviation I could understand. This = mess isn't.


It's not like common lisps practices are that great anyways. It being an old lisp convention, to me, is actually a reason to not follow it.

I want to see a modern take on sexpr based languages instead of being stuck in the past forever.


There is no mention of this being a Lisp anywhere in the name or README files.

You are mistaken in assuming that, since it has parens, then it must conform to being a Lisp.


Here is as good of a place as any to ask: does anyone know of an embeddable scripting language pretty much exactly like Lua but with more C-like syntax?

EDIT: the best alternatives I'd previously found were rhai (still niche and weak library) and sandboxed JS (heavier than I would like and the standard library is still poor).


Maybe Wren? It has classes and method syntax too but it's mostly C like: curly-braced blocks, operators are pretty much the same, slash and slash-asterisk comments, if/while/for with parens, ... Statements are not terminated by semicolons, though.

https://wren.io/syntax.html


For JavaScript, did you look at QuickJS? I believe the binary size is in the same range as Lua.

The standard library isn’t terribly extensive, but I think it’s more comprehensive than Lua, no?


I haven’t heard of that. Something that just compiles to Lua would probably be the easiest way to achieve that.


Not exactly the requirements your asking but you might be interested in the language I'm writing

github.com/civboot/fngi


Maybe AngelScript?


cool a lisp clone. Good work.

Could macros be used to add structs? How much code would this require?

I don't feel like a language is real until you can associate data. It's literally the first thing I do when I start any software project that's not a five line script


i always go to rxi if I want to read some good concise c code, every one is worth reading


Are there any projects using this?


cel7 is a 60kb framework for making grid-based games limited to 4-bit color ASCII output. The framework is based around the fe programming language:

https://rxi.itch.io/cel7

There is a community edition which can be used, as the original is closed source:

https://github.com/kiedtl/cel7ce

There is also "aq", a small framework for making audio toys:

https://github.com/rxi/aq


Wondering how it compares to Lua


Almost two orders of magnitude smaller codebase.


car? cdr? bye!


What do you think HN is written in?


lisp is great, I'm currently learning Janet, playing with fennel and clojure.

But a new 'lispish' should not replicate ancient historical 'accidents', IMHO, and in fact lisp itself should deprecate these names (and caar, caadr, caaaddr ... OMG).

first, rest, or head, tail, or whatever please.


They are a pair rather than a linked list, you could have a list in first and an item in tail. People just usually see them as lists. But they are binary trees.

Left and right might make sense. But then you get into endianness.


Indeed lisp is built on these tree nodes, pairs, tuples, cons cells, whatever. We should rename it 'treep'? 'nodep'? Lol


CAR and CDR can be used to do things other than take lists apart. That's why they have those names.


They have those names because they relate to the registers on the original implementation machine.

time to move on.


John MacCarthy knew that not every computer is an IBM 704, and that users want higher level languages developed on one system to be usable on another.

He stuck with those names because there is no problem with them. They are hard to improve upon in a ways that are only subjectively better while being objectively worse. For instance first and rest are subjectively better for some uses and for some people, while being objectively worse (wastefuly longer for something commonly needed, and falsely associating when the objects aren't used as linear lists). Thus at best we can adopt them as synonyms (and we have).

The were inspired by FLPL ("Fortran List Processing Language"): a Fortran library for list manipulation. FLPL, for mysterious reasons, used pretty horrible names like XCARF and XCDRF. MacCarthy must have been thinking about good naming, because he thankfully dropped the X...F.

I think if they were called something else, I'd still want them to be three-letter identifiers (not just one or two, to avoid clashes) which differ only in the middle letter, so a pattern similar to cadr, cddr ... is possible.

In quantum mechanics there is a <a|b> notation where a is the "bra" and b is the "ket". That doesn't have the right property for cadr, and would be the the target of bra and fig leaf jokes.

In TAOCP, I seem to recall, Knuth calls the fields of binary list cells alink and blink. Not sure if he wants us to pronounce that as blink (of an eye) or bee link. The names are a bit long, but aaabalink is possible. If shortened to alk and blk, that would work.

If both middle letters were vowels, that would be more conducive for the words being pronounceable. That restricts us to aiueo and y. The car using a high vowel is nice, so let's go with i.

How about: dit and dot? The dot is in the ... dot position!

One problem is that cdar becomes doit, which looks like "do it".


"FLPL, for mysterious reasons, used pretty horrible names like XCARF and XCDRF"

It's not mysterious at all, these were also named after the 704 register scheme.

Your argument that MacCarthy et al couldn't think of accurate names may be valid (naming is one of the hardest things, after all), but there are certainly 'better' names, so more likely these just stuck after the initial implementations.

"so a pattern similar to cadr, cddr ... is possible."

Honestly, these are abominations. Remember the point of high level languages etc is not just so you can write code as fast as possible, but that someone else (maybe your future self) can read and understand it easily.


> It's not mysterious at all

By that remark, I'm referring to the X...F dirt in XCARF, not to the CAR infix that we all understand. Why the authors of FLPL saddled the identifiers with that is puzzling. (Extract the content of the address part of the register function? Why?)

Regarding that "R" in car, though standing for "register", refers to a register in memory; Aontent of the Address part of the Register cell.

A piece of memory can be called a register today (e.g. virtual machines have registers that are actually in memory, and some processors have had memory mapped registers like zero page on the 6502). "Content" continues to apply. If someone doesn't like "register", they can pretend it stands for "record". CAR: content of the A part of the record; CDR: content of the D part of the record. The only weird thing are the choices of A and D; the "address" and "data" interpretations don't make sense. But they are just one letter out of three.

Why does it have to be English?

  A = antérieur
  D = derrière
In English we use Latin-derived prefixes where A and D form opposite pairs, like: Ascend/Descend; Approve/Disapprove; Adduce/Deduce .. Or cases where we have different roots: Arrive/Depart. Maybe some interpretation can be found.

In Japanese, car and cdr as the verbs "karu" and "kudaru" work. There are several verbs spelled "karu": 刈る (cut, mow, clip, ...), 借る (borrow) and some others like 狩る (hunt: animals, but also refers to picking and gathering berries and such). Kudaru is 下る, to descend. We can borrow/clip/pick the item at this cell (karu) or descend down (kudaru) to the next cell.

Not everyone who codes is a native speaker, yet most languages use English-derived words. Someone to whom "if" and "while" are foreign words may not care about some English speakers nitpicking over car and cdr.

> can read and understand it easily.

If you accept (car (cdr (cdr x))), then (caddr x) is a nice abbreviation for it, related by a transparent naming scheme. The source code is smaller, and if the compiler doesn't automatically reduce (car (cdr (cdr x))) to caddr, you get better code density.

The name doesn't work in a filter pipeline (-> x caddr ...) because the letters are now backwards w.r.t. pipe direction, since they indicate right to left application.

A good way for your future self to understand everything is not to keep changing names every year. The future self of some person coding in 1968 understands (if still alive) code I just wrote in a different Lisp dialect in 2023, because I used (cadr x). It may not be familiar to Lisp outsiders, but it's an utterly unmoving target for insiders.


You offer no actual arguments, just pronouncements and irrelevant history.


The argument is the names make no sense, they are purely historical.


They make perfect sense once you understand what they do.

They're not mnemonic for any specific usage because they have more than one possible specific usage in the language.

Besides, what kind of sense does '+' make for addition? It's also purely historical.


Imagine you were tasked with implementing a linked-list in, let's say 'C', but anything really.

So you choose 'cell' as your name for a node. OK, that's .. fine.

Now you choose 'car' and 'cdr' for each cell's 'data pointer' and 'next-cell-in-list pointer'. How many 'WTF?s'* do you think you'd get in code review? (except from lispers, I guess)

* obviously it's a rhetorical question, no-one writes or says WTF (out loud) in actual code reviews I hope.


> Now you choose 'car' and 'cdr' for each cell's 'data pointer' and 'next-cell-in-list pointer'. How many 'WTF?s'* do you think you'd get in code review?

Your names are too verbose.


Cons cells aren't a linked list.


struct node_t* node

node->data

node->next

Zero "WTF"s


Except those names are wrong in some contexts. That's the point.


You are tasked with writing a binary tree, and you pick car and cdr ... A key-value tuple-space... Complex numbers...

The point is they are 'wrong' in every context. For LISt Processing, they should probably default to something to do with lists.

And caaar et al are a fine example of why I completely skipped common lisp in favour of newer lisp-likes. Perhaps it happens to all languages eventually, people add a convenience here, an shortcut there, and a doodad here, and before long you have an impenetrable thicket.

And these things seem convenient, but they are a huge extra cognitive load, and impose a further learning cost even to practicing experts who have to keep up with the latest doodads.


Who cares what HN is written in? It could be written in anything. No, I do not buy the Blub programmer argument, either. Yes, maybe literal geniuses are attracted by exotic languages, but genius alone makes for poor business decisions.


Just another LISP clone...


Yes, and? What's wrong with that?


nothing's wrong with another lisp clone.

the problem is with not being upfront about it. The problem is the implication that this is something new, that may solve some new problem in some new way.

Now, it MAY do something other lisp clones don't, but if it does there's zero indication of it in the readme.


"new" when talking about a repository with 15 commits, the last one being 3 years ago.

It is obviously a toy experiment and it took my 5s to realize that.

While it does not say "this is a lisp", it also does not say "this is a new shiny thing that will change the world". In fact, it says nothing except "look at what i did".




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: