Liyad
Liyad (Lisp yet another DSL interpreter) is very small Lisp interpreter written in JavaScript.
Install / Use
/learn @shellyln/LiyadREADME
Liyad
Let's make your yet another DSL with Lisp S-expression!
Liyad (Lisp yet another DSL interpreter, or LIYAD is yum and delicious) is
very small Lisp interpreter written in JavaScript.
You can easily start making your new DSL using Lisp and S-expression.
Install
from NPM:
$ npm install liyad --save
or download UMD from release page.
NOTICE:
Use withwebpack >= 5If you get the error:
Module not found: Error: Can't resolve '(importing/path/to/filename)' in '(path/to/node_modules/path/to/dirname)' Did you mean '(filename).js'?`Add following setting to your
webpack.config.js.{ test: /\.m?js/, resolve: { fullySpecified: false, }, },On
webpack >= 5, the extension in the request is mandatory for it to be fully specified if the origin is a '.mjs' file or a '.js' file where the package.json contains '"type": "module"'.
Install CLI
See liyad-cli .
$ npm install -g liyad-cli
$ liyad
Playground
https://shellyln.github.io/liyad/playground.html
Features
- APIs to customize all operators and macros
- Builtin S-expression parser
- Builtin minimal Lisp interpreter
- Reference implementation of LSX (alternative JSX notation using Lisp)
Real world examples
- Ménneu
Component-based extensible document processor - mdne - Markdown Neo Edit
A simple markdown and code editor powered by Markdown-it, Ace and Carlo. - Tynder
TypeScript friendly Data validator for JavaScript.
What is LSX
LSX is an alternative JSX notation using Lisp.
LSX and Liyad advantages:
-
No transpiler needed
- Liyad uses ES6 template literal syntax.
You don't pass the entire code to transpile and evaluate it.
Save your coding times.
- Liyad uses ES6 template literal syntax.
-
Secure execution for untrusted contents
- No host environment's symbols are accessible from evaluated user contents by default.
Malicious codes can not make a serious attack.
- No host environment's symbols are accessible from evaluated user contents by default.
-
Simple and powerful
- What you can do with JSX can be done with LSX.
Plus, LSX itself is a complete data description format and is a complete programming language,
so you can write more concise and powerful.
- What you can do with JSX can be done with LSX.
The LSX runtime directly calls React.createElement
(or a JSX Factory function such as
RedAgate,
Vue.js, etc.) as a Lisp function,
Convert a Lisp list to a renderer component object tree.
In order to resolve the renderer component, you must register the object's constructor with the LSX runtime in advance.
All unresolved lisp function symbols are dispatched to React.createElement('some_unresolved_name', ...).
You can declare HTML/XML standard tags.
As with JSX, LSX must always return a single component.
Using Template Lisp function instead of JSX Fragment tag will produce the same result.
Example:
lsx`
(Template
(select (@ (style (display "inline-block")
(width "300px") )
(className "foo bar baz")
(onChange ${(e) => this.handleExampleSelected(e.target.value)}) )
($=for ${exampleCodes}
($=if (== (% $index 2) 1)
(option (@ (value $index)) ($concat "odd: " ($get $data "name"))) )
($=if (== (% $index 2) 0)
(option (@ (value $index)) ($concat "even: " ($get $data "name"))) ))))`;
See also:
Playground's source code is written in LSX.
Usage
Output S-expression into JSON:
import { S } from 'liyad';
console.log(
JSON.stringify(S`
($list
1 2 3 "a" "b" "C"
($list 4 5 6) ${"X"} ${["Y", "Z"]} )`
// You can also parse by calling w/o template literal syntax as following:
// S(' ... ')
)
);
Output:
[{"symbol":"$list"},1,2,3,"a","b","C",[{"symbol":"$list"},4,5,6],{"value":"X"},{"value":["Y","Z"]}]
Run minimal Lisp interpreter:
import { lisp } from 'liyad';
console.log(
JSON.stringify(lisp`
($defun fac (n)
($if (== n 0)
1
(* n ($self (- n 1))) ))
($list
1 2 (fac 3) "a" "b" "c"
($list 4 5 (fac 6) ${"X"} ${["Y", "Z"]}) )`
// You can also evaluate by calling w/o template literal syntax as following:
// lisp(' ... ')
)
);
Output:
[1,2,6,"a","b","c",[4,5,720,"X",["Y","Z"]]]
Render web page with LSX:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { LSX } from 'liyad';
var lsx = null;
const exampleCodes = [{
name: "Example1: factorial",
code: ` ... `
}, {
name: "Example2: Hello, World!",
code: ` ... `,
}];
class ExampleLoader extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {};
}
handleExampleSelected(i) {
this.props.loadExample(i);
}
render() {
return (lsx`
(Template
(select (@ (style (display "inline-block")
(width "300px") )
(onChange ${(e) => this.handleExampleSelected(e.target.value)}) )
($=for ${exampleCodes}
(option (@ (value $index)) ($get $data "name")) )))`);
}
}
class App extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {};
}
loadExample(i) {
console.log(exampleCodes[i].code);
}
render() {
return (lsx`
(Template
(div (@ (style (margin "4px")))
(ExampleLoader (@ (loadExample ${(i) => this.loadExample(i)}))) ))`);
}
}
var lsx = LSX({
jsx: React.createElement,
jsxFlagment: React.Fragment,
components: {
ExampleLoader,
App,
},
});
ReactDOM.render(lsx`(App)`, document.getElementById('app'));
Build your new DSL:
import { SxFuncInfo,
SxMacroInfo,
SxSymbolInfo,
SExpression,
SxParserConfig,
defaultConfig,
installCore,
installArithmetic,
installSequence } from 'liyad';
const myOperators: SxFuncInfo[] = [{
name: '$__defun',
fn: (state: SxParserState, name: string) => (...args: any[]) => {
// S expression: ($__defun 'name '(sym1 ... symN) 'expr ... 'expr)
// -> S expr : fn
const car: SxSymbol = $$first(...args);
if (args.length < 3) {
throw new Error(`[SX] $__defun: Invalid argument length: expected: ${3} / args: ${args.length}.`);
}
const fn = $__lambda(state, name)(...args.slice(1));
state.funcMap.set(car.symbol, {
name: car.symbol,
fn: (st, nm) => fn
});
return fn;
},
}];
const myMacros: SxMacroInfo[] = [{
name: '$defun',
fn: (state: SxParserState, name: string) => (list) => {
// S expression: ($defun name (sym1 ... symN) expr ... expr)
// -> S expr : ($__defun 'name '(sym1 ... symN) 'expr ... 'expr)
return [{symbol: '$__defun'},
...(list.slice(1).map(x => quote(state, x))),
];
},
}];
const mySymbols: SxSymbolInfo[] = [
{name: '#t', fn: (state: SxParserState, name: string) => true}
];
export const MyDSL = (() => {
let config: SxParserConfig = Object.assign({}, defaultConfig);
config = installCore(config);
config = installArithmetic(config);
config = installSequence(config);
config.stripComments = true;
config.funcs = (config.funcs || []).concat(myOperators);
config.macros = (config.macros || []).concat(myMacros);
config.symbols = (config.symbols || []).concat(mySymbols);
return SExpression(config);
})();
console.log(
JSON.stringify(MyDSL`( ... )`)
);
Extended syntax
Comments
# This is a line comment
(# ; <-- This is a object literal, not a line comment
)
; This is a line comment
#|
This is a block comment
|#
Here document:
lisp preset interpreter:
"""
Hello, Liyad!
"""
is equivalent to:
($concat
"
Hello, Liyad!
"
)
LSX preset interpreter:
"""
Hello, Liyad!
"""
is equivalent to:
(Template
"
Hello, Liyad!
"
)
Templateon theLSXpreset interpreter, it is mapped to the function passed byLsxConfig.JsxFragment.
See also: Fragments (React), Template (RedAgate).
