Last active
August 6, 2024 20:38
-
-
Save jcubic/ca6548847137584138823f3ba90a002a to your computer and use it in GitHub Desktop.
Simple S-Expression (lisp) paser in JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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