Skip to content

Instantly share code, notes, and snippets.

@jcubic
Last active November 18, 2024 14:07
Show Gist options
  • Save jcubic/ca6548847137584138823f3ba90a002a to your computer and use it in GitHub Desktop.
Save jcubic/ca6548847137584138823f3ba90a002a to your computer and use it in GitHub Desktop.
Simple S-Expression (lisp) paser in JavaScript
console.log(parse(`(define (square x)
"Function calculate square of a number"
(* x x))`)[0].toString());
// ==> (define (square x) "funkcja square wywołanie (square 10) zwraca 100" (* x x))
// Copyright (C) 2024 Jakub T. Jankiewicz <https://jcubic.pl/me>
// Released under MIT license
const int_re = /^[-+]?[0-9]+([eE][-+]?[0-9]+)?$/;
const float_re = /^([-+]?((\.[0-9]+|[0-9]+\.[0-9]+)([eE][-+]?[0-9]+)?)|[0-9]+\.)$/;
const tokens_re = /("[^"\\]*(?:\\[\S\s][^"\\]*)*"|\(|\)|\n|\s+|[^\s()]+)/;
class Pair {
constructor(head, tail) {
this.head = head;
this.tail = tail;
}
toString() {
const arr = ['('];
if (this.head) {
let value;
if (typeof this.head === 'string') {
value = JSON.stringify(this.head).replace(/\\n/g, '\n');
} else {
value = this.head.toString(); // any object including Pair and LSymbol
}
arr.push(value);
if (this.tail instanceof Pair) {
// replace hack for nested list because structure is a tree
// and here tail is next element
const tail = this.tail.toString().replace(/^\(|\)$/g, '');
arr.push(' ');
arr.push(tail);
}
}
arr.push(')');
return arr.join('');
}
static fromArray(array) {
if (!array.length) {
return nil;
}
let [head, ...rest] = array
if (head instanceof Array) {
head = Pair.fromArray(head);
}
return new Pair(head, Pair.fromArray(rest));
}
}
class LSymbol {
constructor(name) {
this.name = name;
}
toString() {
return this.name;
}
}
class Nil {}
const nil = new Nil();
function tokenize(string) {
string = string.trim();
if (!string) {
return [];
}
return string.split(tokens_re).filter(token => token.trim());
}
function parse(string) {
const tokens = tokenize(string);
const result = [];
const stack = [];
tokens.forEach(token => {
if (token == '(') {
stack.push([]);
} else if (token == ')') {
if (stack.length) {
const top = stack.pop();
if (stack.length) {
const last = stack[stack.length - 1];
last.push(top);
} else {
result.push(top);
}
} else {
throw new Error('Syntax Error - unmached closing paren');
}
} else {
const top = stack[stack.length - 1];
top.push(parseAtom(token)); // this line was added
}
});
if (stack.length) {
throw new Error('Syntax Error - expecting closing paren');
}
return result.map(Pair.fromArray);
}
function parseAtom(atom) {
if (atom.match(int_re) || atom.match(float_re)) { // numbers
return parseFloat(atom);
} else if (atom.match(/^".*"$/)) {
return JSON.parse(atom.replace(/\n/g, '\\n')); // strings
} else {
return new LSymbol(atom); // symbols
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment