npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lykn/lykn

v0.3.0

Published

S-expression syntax for JavaScript. A lightweight Lisp that compiles to clean JS.

Readme

lykn

S-expression syntax for JavaScript

lykn is a lightweight Lisp that compiles to clean, readable JavaScript. No runtime, no dependencies in the output — just JS you'd write by hand, but expressed in s-expressions.

lykn has two syntax layers: surface syntax for everyday code (typed functions, algebraic data types, pattern matching, immutable bindings) and kernel syntax for low-level control. Both are s-expressions; surface forms compile to kernel forms, which compile to JavaScript.

The name means good luck in Norwegian, luck in Swedish, and — if you squint at the Icelandic — closure.

Status

v0.3.0 — Two-layer language design lands. Surface syntax is now the recommended way to write lykn: typed functions with contracts, algebraic data types, exhaustive pattern matching, immutable-by-default bindings with cell for controlled mutation, and threading macros. Kernel syntax remains available for low-level control and JS interop. 73KB browser bundle. 555 tests.

Quick taste

;; Immutable bindings
(bind greeting "hello, world")

;; Typed functions with runtime safety
(func greet
  :args (:string name)
  :returns :string
  :body (+ greeting ", " name "!"))

;; Threading macros
(bind result (-> 5 (+ 3) (* 2)))

;; Objects with keyword syntax
(bind user (obj :name "lykn" :version "0.3.0"))

;; Controlled mutation via cells
(bind counter (cell 0))
(swap! counter (=> (n) (+ n 1)))
(console:log (express counter))

;; Macros still work — define your own forms
(macro when (test (rest body))
  `(if ,test (block ,@body)))

(when (> result 0)
  (console:log "positive"))

Compiles to:

const greeting = "hello, world";
function greet(name) {
  if (typeof name !== "string") throw new TypeError("greet: arg 'name' expected string, got " + typeof name);
  const result__gensym0 = greeting + ", " + name + "!";
  if (typeof result__gensym0 !== "string") throw new TypeError("greet: return 'result__gensym0' expected string, got " + typeof result__gensym0);
  return result__gensym0;
}
const result = (5 + 3) * 2;
const user = {
  name: "lykn",
  version: "0.3.0"
};
const counter = {
  value: 0
};
counter.value = (n => n + 1)(counter.value);
console.log(counter.value);
if (result > 0) {
  console.log("positive");
}

Architecture

.lykn source → reader → surface macros → expander → compiler → astring → JavaScript
  • Reader (src/reader.js) — parses s-expressions, handles # dispatch (`, ,, ,@, #a(...), #o(...), #NNr, #;, #|...|#), dotted pairs

  • Surface macros (src/surface.js) — transforms high-level surface forms (bind, func, type, match, obj, cell, threading macros) to kernel forms before macro expansion

  • Expander (src/expander.js) — three-pass macro expansion pipeline. Resolves quasiquote (Bawden's algorithm), sugar forms (cons/list/ car/cdr), user-defined macros, import-macros, as patterns

  • Compiler (src/compiler.js) — transforms core forms to ESTree AST, generates JS via astring

  • Browser shim (src/lykn-browser.js) — 56KB bundle with <script type="text/lykn"> support and window.lykn API

  • Rust tools (crates/lykn-cli/) — linter, formatter, syntax checker. Single binary, no runtime dependencies. Publishable to crates.io.

Toolchain

brew install biome deno

Lint

# JS (src/)
deno lint src/
biome lint src/

# Rust
cargo clippy

Format

# JS (src/)
biome format src/
biome format --write src/    # fix in place

# Rust
cargo fmt

Test

deno task test              # all tests
deno task test:unit         # unit tests only
deno task test:integration  # integration tests only
cargo test                  # Rust tests

Usage

Browser

<script src="dist/lykn-browser.js"></script>
<script type="text/lykn">
  ;; Macros work inline in the browser!
  (macro when (test (rest body))
    `(if ,test (block ,@body)))

  (const el (document:query-selector "#output"))
  (when el
    (= el:text-content "Hello from lykn!"))
</script>

Or use the API directly:

lykn.compile('(+ 1 2)')   // → "1 + 2;\n"
lykn.run('(+ 1 2)')       // → 3
await lykn.load('/app.lykn')

Note: import-macros is not available in the browser (no file system access). Inline macro definitions work.

Build Browser Bundle

deno task build

Format (Rust)

# Build from source
mkdir -p ./bin
cargo build --release && cp ./target/release/lykn ./bin

# Format a file (stdout)
./target/release/lykn fmt main.lykn

# Format in place
./target/release/lykn fmt -w main.lykn

# Syntax check
./target/release/lykn check main.lykn

Run Examples

# Compile and run the CLI example
deno eval "import {lykn} from './src/index.js'; \
  const src = await Deno.readTextFile('examples/surface/main.lykn'); \
  eval(lykn(src))"

# Serve the browser examples (needs a local server for external .lykn files)
deno run --allow-net --allow-read jsr:@std/http@1/file-server --port 5099
# Then open http://localhost:5099/examples/surface/browser.html

Both examples/surface/ (recommended) and examples/kernel/ are available.

Supported forms

Surface forms

Surface syntax is the recommended way to write lykn. These forms expand to kernel forms at compile time.

Bindings & mutation

| lykn | JS | |---|---| | (bind x 1) | const x = 1; | | (bind counter (cell 0)) | const counter = { value: 0 }; | | (swap! counter f) | counter.value = f(counter.value); | | (reset! counter 0) | counter.value = 0; | | (express counter) | counter.value |

Functions

| lykn | JS | |---|---| | (func add :args (:number a :number b) :returns :number :body (+ a b)) | function add(a, b) { ... return a + b; } with type checks | | (func now (Date:now)) | function now() { return Date.now(); } | | (fn (:number x) (* x 2)) | x => { ...; x * 2; } with type check |

Types & pattern matching

| lykn | JS | |---|---| | (type Option (Some :any value) None) | Constructor functions with { tag: "Some", value } | | (match opt ((Some v) v) (None fallback)) | Exhaustive if-chain on .tag | | (if-let ((Some user) (find id)) (greet user) "none") | Tag check + binding + branch | | (when-let ((Some user) (find id)) (greet user)) | Same without else branch |

Objects

| lykn | JS | |---|---| | (obj :name "x" :age 42) | { name: "x", age: 42 } | | (assoc user :age 43) | { ...user, age: 43 } | | (dissoc user :password) | Spread + delete | | (conj items new-item) | [...items, newItem] |

Threading macros

| lykn | JS | |---|---| | (-> x (+ 3) (* 2)) | (x + 3) * 2 | | (->> items (filter even?) (map double)) | map(filter(items, even), double) | | (some-> user (get :name) (str:to-upper-case)) | IIFE with null checks at each step |

Kernel forms

Kernel forms are the compilation targets for surface macros. You can use them directly for low-level control, JS interop, or when surface syntax doesn't cover a specific JS feature.

Basics

| lykn | JS | |---|---| | (const x 1) | const x = 1; | | (let x 1) | let x = 1; | | my-function | myFunction | | console:log | console.log | | this:-name | this.#_name | | (get arr 0) | arr[0] |

Functions

| lykn | JS | |---|---| | (=> (a b) (+ a b)) | (a, b) => a + b | | (function add (a b) (return (+ a b))) | function add(a, b) { return a + b; } | | (lambda (a) (return a)) | function(a) { return a; } | | (async (=> () (await (fetch url)))) | async () => await fetch(url) | | (=> ((default x 0)) x) | (x = 0) => x | | (function f (a (rest args)) ...) | function f(a, ...args) { ... } |

Modules

| lykn | JS | |---|---| | (import "mod" (a b)) | import {a, b} from "mod"; | | (import "mod" name) | import name from "mod"; | | (export (const x 42)) | export const x = 42; | | (export default my-fn) | export default myFn; | | (dynamic-import "./mod.js") | import("./mod.js") |

Control flow

| lykn | JS | |---|---| | (if cond a b) | if (cond) a; else b; | | (? test a b) | test ? a : b | | (for-of item items (f item)) | for (const item of items) { f(item); } | | (while cond body...) | while (cond) { body } | | (try body (catch e ...) (finally ...)) | try { body } catch(e) { ... } finally { ... } | | (switch x ("a" (f) (break)) (default (g))) | switch(x) { case "a": f(); break; default: g(); } | | (throw (new Error "oops")) | throw new Error("oops"); |

Expressions

| lykn | JS | |---|---| | (template "hi " name "!") | `hi ${name}!` | | (tag html (template ...)) | html`...` | | (object (name "x") age) | {name: "x", age} | | (array 1 2 (spread rest)) | [1, 2, ...rest] | | (regex "^hello" "gi") | /^hello/gi | | (new Thing a b) | new Thing(a, b) |

Destructuring

| lykn | JS | |---|---| | (const (object name age) person) | const {name, age} = person; | | (const (array first (rest tail)) list) | const [first, ...tail] = list; | | (const (object (alias data items)) obj) | const {data: items} = obj; | | (const (object (default x 0)) point) | const {x = 0} = point; | | (const (array _ _ third) arr) | const [, , third] = arr; |

Classes

| lykn | JS | |---|---| | (class Dog (Animal) ...) | class Dog extends Animal { ... } | | (field -count 0) | #_count = 0; | | (get area () (return x)) | get area() { return x; } | | (static (field count 0)) | static count = 0; | | (async (fetch-data () ...)) | async fetchData() { ... } |

Operators

| lykn | JS | |---|---| | (+ a b c) | a + b + c | | (++ x) | ++x | | (+= x 1) | x += 1 | | (** base exp) | base ** exp | | (?? a b) | a ?? b |

Macros

| lykn | What it does | |---|---| | (macro when (test (rest body)) `(if ,test (block ,@body))) | Define a macro with quasiquote template | | (import-macros "./lib.lykn" (when unless)) | Import macros from another file | | `(if ,test ,@body) | Quasiquote with unquote and splicing | | temp#gen | Auto-gensym (hygienic binding) | | (gensym "prefix") | Programmatic gensym |

Data literals and sugar

| lykn | JS | |---|---| | #a(1 2 3) | [1, 2, 3] | | #o((name "x") (age 42)) | {name: "x", age: 42} | | #16rff | 255 (radix literal) | | #2r11110000 | 240 (binary) | | (cons 1 2) | [1, 2] | | (list 1 2 3) | [1, [2, [3, null]]] | | (car x) / (cdr x) | x[0] / x[1] | | #; expr | Expression comment (discards next form) | | #\| ... \|# | Nestable block comment |

Design principles

  • Thin skin over JS. lykn is not a new language. It's a syntax for the language you already have. The output should look like code you'd write.
  • No runtime. Compiled lykn is just JS. Nothing extra ships to the browser.
  • Small tools. The full pipeline (reader + expander + compiler) is ~3,000 lines. The browser bundle is 56KB minified. You can read the whole thing.
  • Two worlds. Use Rust for dev-side tooling (fast, single binary). Use JS for the compiler (because it targets JS and can run in the browser).

References

  • ESTree spec — the AST format lykn targets
  • astring — ESTree to JS code generation
  • Bawden 1999 — "Quasiquotation in Lisp", the algorithm behind lykn's macro expansion
  • Fennel — inspiration for enforced gensym hygiene model
  • eslisp — spiritual ancestor; reference implementation
  • BiwaScheme — inspiration for the in-browser <script> workflow

License

Apache-2.0