Skip to content

piglet-lang/piglet

Repository files navigation

Piglet

Piglet is a ES6-native LISP. In true LISP tradition it is in the first place envisaged as a live environment, meaning the Reader, Compiler, and any other language-level utilities are available at runtime.

$ npm install -g piglet-lang
$ piglet-lang
piglet:lang=> (eval (read-string "(+ 1 1)"))
2
piglet:lang=> *current-module*
https://piglet-lang.org/packages/piglet:lang
piglet:lang=> (set! *qname-print-style* :full) 
:full
piglet:lang=> (print-str :foaf:name)
":http://xmlns.com/foaf/0.1/name"

Piglet is strongly influenced by Clojure, but it does not try to be a Clojure implementation. It's a different language, allowing us to experiment with some novel ideas, and to improve on certain ergonomics.

Architecture

Piglet is not just a transpiler, but a full compiler and runtime implemented directly in the host language (JavaScript). In that sense it is closer to Clojure than to ClojureScript. Piglet provides full introspection over packages/modules/vars.

Compilation happens by first reading strings to forms (S-expressions). The Analyzer converts these to a Piglet AST. This is then converted to a ESTree-compliant JavaScript AST, which is then converted to JavaScript. This last step is currently handled by astring.

Piglet heavily leans into protocols. Many core functions are backed by protocol methods, and these protocols are extended to many built-in JavaScript types, providing very smooth interop.

Keywords and Symbols, QNames and QSyms

Piglet has plain unqualified symbols, which evaluate to local variables or var lookups, like any other LISP. It also has plain keywords, which start with a colon, and are simple interned identifiers that evaluate to themselves, like unqualified keywords in Clojure. As in Clojure these behave like functions which perform a keyword lookup (works both for Piglet data structures and plain JS maps).

Clojure also has qualified or namespaced symbols and keywords. The former as a way to identify a var within a namespace, the latter as a fully qualified identifier, allowing keys in dictionary-like objects to be less prone to collision, and able to carry precise semantics for the associated value. Both symbols and keywords can be abbreviated based on namespace aliases for terseness.

One of the ideas we wanted to explore with Piglet is: what if Clojure had went all the way and used full URIs as identifiers, both for data, and for code (var resolution)? The result are QNames as an alternative to qualified keywords, and QSyms as an alternative to qualified symbols.

QName

A QName is a interned absolute URI identifier intended for data representation. Written in their full form as syntax literals they start with a colon, and must contain ://.

:http://xmlns.com/foaf/0.1/name

Fully writing them out like this however is unwieldy. Instead a set of prefixes can be configured, which will be understood by both the reader and the printer. A number of common default prefixes are available, like rdf, owl, foaf, or svg.

piglet:lang=> (fqn :foaf:name)
"http://xmlns.com/foaf/0.1/name"

Both printing and reading are controlled by the *current-context* var, which holds a Dict from prefix to URI. If you've worked with JSON-LD this idea of a current context should be familiar.

The idea is that both RDF identifiers and namespaced XML element names can be represented directly, and that truly globally unique identifiers become the norm, while at the source level one gets to largely ignore this verboseness thanks to alias prefixes.

piglet:lang=> (set! *current-context* (assoc *current-context* "foo" "https://example.com/foo#"))
piglet:lang=> (fqn :foo:bar)
"https://example.com/foo#bar"

Note that the separator here is the colon, as in JSON-LD, not the slash. Slashes hold no special meaning, and keywords (or symbols) can contain any number of slashes.

QSyms, Packages, Modules, Vars

Both on disk and in-memory Piglet code is organized in Packages, Modules, and Vars. Packages are fully first class, rather than being merely a boot-time concern that then all gets lumped together into a single search path. A package has a name, which is a URI (if no name is specified it gets a file:// name based on its location on disk). A package maps to a set of source files.

Each source file within a package defines a piglet Module (and can be compiled to a ES6 module). The module name follows the file name starting from the module's configured source directory (src by default). So a module foo/bar goes into src/foo/bar.pig.

A module defines var (with def or defn), which gets interned into the Module, which in turn is part of the Package, which is stored in the ModuleRegistry.

Vars have fully qualified names, which is the name of the package, module, and var, combined with colons.

piglet:lang=> (fqn #'assoc)
https://piglet-lang.org/packages/piglet:lang:assoc

A package has a package.pig file at the package root.

{:pkg:name https://my-project.org/packages/hello-world
 :pkg:paths ["."]
 :pkg:deps {foolib {:pkg:location "../foolib"}}}

(pkg is a prefix in the default context aliased to https://vocab.piglet-lang.org/package/)

The :pkg:deps is in itself a sort of alias declaration, in this case it's stating that within this hello-world package, the package which is located at ../foolib is aliased to foolib. Now we can load a module from that package, assigning it its own local prefix.

(module foo/bar
  (:import [bar :from foolib:foo/bar]))

Standard library

Some libraries that are bundled with Piglet include

  • piglet:string
  • piglet:dom
  • piglet:cbor

More docs

There is some scattered information under [[doc]].

Copyright (c) Arne Brasseur 2023-2025. All rights reserved.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published