├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── Justfile ├── README.md ├── bin ├── pig └── piglet ├── checklist.org ├── doc ├── ADRs │ ├── 001_record_architectural_decisions.md │ ├── 002_modern_functional_lisp_for_the_javascript_age.md │ ├── 003_depend_on_standards_not_on_implementations.md │ ├── TEMPLATE.md │ └── XXX_compile_lisp_to_estree.md ├── Piglet_Manual.adoc ├── bookmarklet.html ├── built_in_protocols.md ├── class_syntax.md ├── data_types.md ├── destructuring.adoc ├── differences_from_clojure.md ├── getting_started.adoc ├── javascript_interop.md ├── modules_and_packages.md ├── piglet_overview.md ├── porting_clojure_code.md └── quickstart.md ├── lib └── piglet │ ├── browser │ ├── BrowserCompiler.mjs │ └── main.mjs │ ├── lang.mjs │ ├── lang │ ├── AbstractCompiler.mjs │ ├── AbstractIdentifier.mjs │ ├── AbstractSeq.mjs │ ├── Analyzer.mjs │ ├── CodeGen.mjs │ ├── Cons.mjs │ ├── Context.mjs │ ├── Dict.mjs │ ├── HashSet.mjs │ ├── IteratorSeq.mjs │ ├── Keyword.mjs │ ├── LazySeq.mjs │ ├── List.mjs │ ├── Module.mjs │ ├── ModuleRegistry.mjs │ ├── Package.mjs │ ├── PrefixName.mjs │ ├── Protocol.mjs │ ├── QName.mjs │ ├── QSym.mjs │ ├── Range.mjs │ ├── Repeat.mjs │ ├── SeqIterator.mjs │ ├── StringReader.mjs │ ├── Sym.mjs │ ├── Var.mjs │ ├── hashing.mjs │ ├── metadata.mjs │ ├── piglet_object.mjs │ ├── protocols │ │ ├── Associative.mjs │ │ ├── Conjable.mjs │ │ ├── Counted.mjs │ │ ├── Derefable.mjs │ │ ├── DictLike.mjs │ │ ├── Empty.mjs │ │ ├── Eq.mjs │ │ ├── Hashable.mjs │ │ ├── Lookup.mjs │ │ ├── MutableAssociative.mjs │ │ ├── MutableCollection.mjs │ │ ├── Named.mjs │ │ ├── QualifiedName.mjs │ │ ├── Repr.mjs │ │ ├── Seq.mjs │ │ ├── Seqable.mjs │ │ ├── Sequential.mjs │ │ ├── Swappable.mjs │ │ ├── TaggedValue.mjs │ │ ├── Walkable.mjs │ │ └── WithMeta.mjs │ ├── util.mjs │ └── xxhash32.mjs │ └── node │ ├── AOTCompiler.mjs │ ├── NodeCompiler.mjs │ ├── NodeREPL.mjs │ ├── main.mjs │ └── pig_cli.mjs ├── notes.md ├── package.json ├── packages ├── demo │ ├── express_test.pig │ ├── hiccup.pig │ └── package.pig ├── dev-server │ └── package.pig ├── nrepl │ ├── package.pig │ └── src │ │ └── nrepl.pig └── piglet │ ├── package.pig │ └── src │ ├── cbor.pig │ ├── cli │ ├── parseargs.pig │ └── terminal.pig │ ├── css.pig │ ├── dom.pig │ ├── lang.pig │ ├── node │ ├── dev-server.pig │ ├── http-server.pig │ └── pig-cli.pig │ ├── pdp-client.pig │ ├── reactive.pig │ ├── spec │ ├── all.pig │ ├── binding-forms.pig │ ├── destructuring.pig │ └── util.pig │ ├── string.pig │ ├── test.pig │ └── web │ ├── inferno.mjs │ └── ui.pig ├── pnpm-lock.yaml ├── repl_sessions ├── bindings.pig ├── cli-test.pig ├── comments.mjs ├── comments.pig ├── dom_stuff.pig ├── package_stuff.pig ├── print_parse_tree.mjs ├── test.js └── typed_array_copy.pig ├── rollup.config.mjs ├── rollup_piglet.mjs ├── scratch.pig └── webdemo ├── index.html ├── solid-demo.pig ├── solid_test.js ├── src └── dom.pig └── webdemo.pig /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .#* 3 | *.tgz 4 | dist 5 | target 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .#* 3 | *demo* 4 | *scratch* 5 | doc/ADRs/TEMPLATE.md 6 | notes.md 7 | *.tgz 8 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | build: 2 | ./node_modules/.bin/rollup -c 3 | 4 | release: 5 | #!/bin/sh 6 | VERSION="$(pnpm version patch --no-git-tag-version -m 'Release %s')" 7 | VERSION_NUMBER="$(echo -n $VERSION | sed s/v//)" 8 | cat < (eval (read-string "(+ 1 1)")) 13 | 2 14 | piglet:lang=> *current-module* 15 | https://piglet-lang.org/packages/piglet:lang 16 | piglet:lang=> (set! *qname-print-style* :full) 17 | :full 18 | piglet:lang=> (print-str :foaf:name) 19 | ":http://xmlns.com/foaf/0.1/name" 20 | ``` 21 | 22 | Piglet is strongly influenced by Clojure, but it does not try to be a Clojure 23 | implementation. It's a different language, allowing us to experiment with some 24 | novel ideas, and to improve on certain ergonomics. 25 | 26 | Features 27 | 28 | - Supports any ES6 compatible runtime, including browsers and Node.js 29 | - RDF-style fully qualified identifiers both for code and data 30 | - Interactive programming facilities 31 | - Excellent and extensive JS interop, in both directions 32 | - First class vars 33 | - Value-based equality 34 | - Uses wasm for fast hashing 35 | - Metadata on data, functions, vars 36 | - Introspection, meta-programming facilities, macros 37 | - Extensible through protocols 38 | - `Seq` abstraction for sequential data structure access, including lazy/infinite sequences 39 | - Dict and Set datatypes, on top of JS's built-in data types (currently these 40 | are naive copy-on-write implementations, the Set implementation does have a 41 | fast hash-based membership test) 42 | - Sequential and associative destructuring in `def`, `let`, `fn`, and `loop` 43 | - Tail call optimization through `loop`/`recur` 44 | - Extensible reader through tagged literals / data readers 45 | - Built-in libs for dom manipulation, reactive primitives, command line argument handling, CBOR 46 | - AOT compilation that is amenable to tree shaking (experimental/WIP) 47 | - [ES6-class syntax](doc/class_syntax.md) with support for computed properties, static properties/methods/initializers 48 | 49 | ## Getting started 50 | 51 | See [Quickstart](doc/quickstart.md) 52 | 53 | ## Architecture 54 | 55 | Piglet is not just a transpiler, but a full compiler and runtime implemented 56 | directly in the host language (JavaScript). In that sense it is closer to 57 | Clojure than to ClojureScript. Piglet provides full introspection over 58 | packages/modules/vars. 59 | 60 | Compilation happens by first reading strings to forms (S-expressions). The 61 | Analyzer converts these to a Piglet AST. This is then converted to a 62 | [ESTree](https://github.com/estree/estree)-compliant JavaScript AST, which is 63 | then converted to JavaScript. This last step is currently handled by 64 | [astring](https://github.com/davidbonnet/astring). 65 | 66 | Piglet heavily leans into [protocols](doc/built_in_protocols.md). Many core 67 | functions are backed by protocol methods, and these protocols are extended to 68 | many built-in JavaScript types, providing very smooth interop. 69 | 70 | ## Docs 71 | 72 | There is some more scattered information under [doc](doc/). It's a bit of a 73 | hodgepodge mess at the moment. 74 | 75 | ## What's not there yet 76 | 77 | A lot. Piglet is at the point where there's enough there to be at least 78 | interesting, and perhaps even useful, but it is an ambitious project that is 79 | currently not explicitly funded. Here are some of the things we hope to 80 | eventually still get to. 81 | 82 | - A pragma system to influence aspects of compilation on the package and module level 83 | - Pluggable data structure literals, e.g. pragmas to compile dict literals to plain JS object literals 84 | - Pragmas to compile destructuring forms to JS destructuring 85 | - Full functional data structures, especially Dict 86 | - There's currently no Vector implementation, we just use JS arrays (and that might be fine) 87 | - Integration with ES build tools for one-stop-shop optimized compilation 88 | - Hot code reloading (while an interactive programming workflow is generally superior, hot code reloading is very useful when doing UI work) 89 | - The Tree-sitter grammar has some rough edges 90 | - Support for editors beyond emacs (especially the ones that support tree-sitter) 91 | 92 | ## License 93 | 94 | We have not yet determined the right license to release Piglet under, so while 95 | the source is freely available, Piglet is not currently Free Software or Open 96 | Source software. 97 | 98 | If your company or organization is interested in funding Piglet development, get 99 | in touch! 100 | 101 | If you have interest in using Piglet for a commercial project please reach out 102 | to [Gaiwan](https://gaiwan.co). 103 | 104 | Copyright (c) Arne Brasseur 2023-2025. All rights reserved. 105 | -------------------------------------------------------------------------------- /bin/pig: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Using a bash-ism here because to do this in POSIX in the face of symlinks is 4 | # _very_ tedious, see https://stackoverflow.com/a/29835459 5 | PIGLET_HOME="$(dirname "$(realpath $0)")/.." 6 | exec node "${PIGLET_HOME}/lib/piglet/node/pig_cli.mjs" "$@" 7 | -------------------------------------------------------------------------------- /bin/piglet: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Convenience entrypoint "for now". You can symlink this from somewhere on your 4 | # PATH so you can call `piglet` from anywhere. This is also the main executable 5 | # we expose for npm/npx. 6 | # 7 | # Adds a `--devtools` option which runs piglet through nodemon, so you get hot 8 | # reload in node while working on piglet itself, and it runs node with the 9 | # --inspect flag, so you can connect from chrome devtools. 10 | 11 | # Using a bash-ism here because to do this in POSIX in the face of symlinks is _very_ tedious, see https://stackoverflow.com/a/29835459 12 | PIGLET_HOME="$(dirname "$(realpath $0)")/.." 13 | 14 | RLWRAP="$(which rlwrap)" 15 | 16 | if [ -z "$RLWRAP" ]; then 17 | echo "rlwrap not found on PATH, make sure it's installed for history and line editing in the REPL." 18 | fi 19 | 20 | if [ "$1" = "--devtools" ]; then 21 | shift 22 | # not using npx because it messes up rlwrap 23 | if [ -z "$(which nodemon)" ]; then 24 | pnpm i -g nodemon 25 | fi 26 | 27 | if [ -z "$RLWRAP" ]; then 28 | exec nodemon --watch "${PIGLET_HOME}" --inspect "${PIGLET_HOME}/lib/piglet/node/main.mjs" "--" "$@" 29 | fi 30 | exec "$RLWRAP" nodemon --watch "${PIGLET_HOME}" --inspect "${PIGLET_HOME}/lib/piglet/node/main.mjs" "--" "$@" 31 | else 32 | if [ -z "$RLWRAP" ]; then 33 | exec node "${PIGLET_HOME}/lib/piglet/node/main.mjs" "$@" 34 | fi 35 | exec "$RLWRAP" node "${PIGLET_HOME}/lib/piglet/node/main.mjs" "$@" 36 | fi 37 | -------------------------------------------------------------------------------- /checklist.org: -------------------------------------------------------------------------------- 1 | - pure JS 2 | - leverage JS ecosystem 3 | - no compilation needed 4 | 5 | - pluggable datastructures 6 | - fully qualified naming 7 | 8 | :foo 9 | 10 | :foo.bar.baz/foo 11 | 12 | 13 | (:foaf:gender {:http://xmlns.com/foaf/0.1/gender "foo"}) 14 | 15 | 16 | 17 | 18 | 19 | ** Syntax and features 20 | 21 | - [X] new instance syntax 22 | - [X] Package imports 23 | - [X] PrefixName to QName expansion 24 | - [X] varargs 25 | - [X] Module expansion context 26 | - [X] dictionary syntax 27 | 28 | - [-] let and destructuring 29 | - [X] list / array destructuring 30 | - [ ] dict destructuring 31 | - [X] metadata syntax 32 | - [X] JS imports (string) 33 | - [X] constructor shorthand 34 | - [X] proper printer 35 | - [X] print dicts 36 | - [X] with-meta 37 | - [X] truthy? -> use for conditionals 38 | - [ ] backtick/unquote/unquote-splice 39 | - [ ] dynamic var bindings 40 | - [ ] pluggable data structures 41 | - [ ] var shorthand 42 | - [ ] vary-meta 43 | - [ ] try/catch 44 | - [ ] AOT compilation to modules 45 | 46 | 47 | ** API 48 | 49 | - [X] range 50 | - [X] repeat 51 | - [X] various convenience functions 52 | - [X] inc 53 | - [X] dec 54 | - [-] threading macros 55 | - [X] -> 56 | - [X] ->> 57 | - [ ] <<- 58 | - [ ] some-> 59 | - [ ] some->> 60 | - [ ] as-> 61 | - [X] cond-> 62 | - [ ] cond->> 63 | - [-] predicates 64 | - [ ] some? 65 | - [X] array? 66 | - [-] full set of seq operations (filter/remove/some) 67 | - [X] filter 68 | - [X] remove 69 | - [ ] some 70 | - [ ] keep 71 | - [ ] collection operations (includes?/has-key?/assoc/dissoc/disj) 72 | - [ ] seq/collection macros (doseq/for) 73 | - [ ] doseq 74 | - [-] functional composition 75 | - [ ] comp 76 | - [ ] juxt 77 | - [X] complement 78 | - [ ] fnil 79 | - [X] identity 80 | -------------------------------------------------------------------------------- /doc/ADRs/001_record_architectural_decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architectural decisions 2 | 3 | Date: 2023-01-19 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | ## Consequences 18 | 19 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 20 | 21 | -------------------------------------------------------------------------------- /doc/ADRs/002_modern_functional_lisp_for_the_javascript_age.md: -------------------------------------------------------------------------------- 1 | # 2. A Modern Functional LISP for the Web Age 2 | 3 | Date: 2023-06-12 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to outline the vision and high level goals of the Piglet project. 12 | 13 | ## Decision 14 | 15 | These are the ideas that we base the design of Piglet on: 16 | 17 | Piglet is a LISP, a dynamically typed programming language with late binding, 18 | support for interactive programming ("REPL"), compile-time programming through 19 | macros, with reflection and introspection facilities. All aspects of the 20 | language are available at runtime, including the compiler, reader (parser), and 21 | the module system. 22 | 23 | Piglet is a Modern LISP, with rich reader literals, a full-features standard 24 | library powered by extensible protocols, threading macros, and destructuring. 25 | 26 | Piglet is a functional language, with a data model focused on immutability, 27 | including vectors, dicts, sets, lists, symbols, keywords, and fully-qualified 28 | identifiers. 29 | 30 | Piglet is designed for the web age. It is implemented as EcmaScript modules, and 31 | can run in any compliant EcmaScript runtime, including web browsers, Node.js, 32 | and elsewhere. It can interoperate bidirectionally with JavaScript code, and 33 | includes facilities for making interoperability convenient. 34 | 35 | Piglet embraces web standards, including EcmaScript, the W3C DOM, URI, and RDF. 36 | 37 | ## Consequences 38 | 39 | These decisions make Piglet an appealing language for a wide range of use cases, 40 | and make it a technology that can be applied in a wide range of contexts. 41 | -------------------------------------------------------------------------------- /doc/ADRs/003_depend_on_standards_not_on_implementations.md: -------------------------------------------------------------------------------- 1 | # 3. Depend on Standards, not Implementations 2 | 3 | Date: 2023-06-12 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We want Piglet to have a long and prosperous life, and to avoid changes being 12 | forced upon us by third parties. 13 | 14 | ## Decision 15 | 16 | The core language implementation will not have any concrete dependencies, like 17 | specific libraries or tools. We only depend on specified and accepted standards, 18 | where multiple compatible implementations exist. 19 | 20 | Currently the standards we rely upon are: 21 | 22 | - EcmaScript, as defined by TC39 23 | - ESTree, as defined by the ESTree steering committee 24 | 25 | ## Consequences 26 | 27 | This decision ensures that you can run Piglet directly from source, and compile 28 | and evaluate forms and modules, with no additional libraries or tooling, given a 29 | compliant JavaScript runtime, and a library that can covert ESTree to 30 | JavaScript, like astring, or escodegen. 31 | 32 | This decision is only for the core implementation, we will provide convenience 33 | wrappers for specific contexts and environments, which have concrete 34 | dependencies, to provide a good out-of-the-box experience. 35 | 36 | This decision will ensure that, as the world around us changes, Piglet can 37 | continue to find a place in it. 38 | -------------------------------------------------------------------------------- /doc/ADRs/TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # 1. Succint but clear title 2 | 3 | Date: YYYY-DD-MM 4 | 5 | ## Status 6 | 7 | Accepted | Superseded by ADR-XXX 8 | 9 | ## Context 10 | 11 | Sketch the context that this decision emerged in. 12 | 13 | ## Decision 14 | 15 | Record the actual decision. 16 | 17 | ## Consequences 18 | 19 | Think two steps ahead, what will the first and second order effects of this decision be? 20 | -------------------------------------------------------------------------------- /doc/ADRs/XXX_compile_lisp_to_estree.md: -------------------------------------------------------------------------------- 1 | # 2. Compile Lisp to EStree 2 | 3 | Date: 2022-03-20 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | In the last decade there has been renewed interest in the programming model of 12 | Lisp, in large part due to the arrival of modern implementations that tap into 13 | the power of a host language and platform, and that provide high quality 14 | functional data structures, and quality of life syntactical additions. 15 | 16 | We want to continue on this tradition, while providing greater accessibility, 17 | and allowing for experimentation with new language features. 18 | 19 | ## Decision 20 | 21 | The goal of the project is to create a compiler for a Lisp dialect, using pure 22 | EcmaScript (ES6), which emits a standardized EcmaScript/JavaScript AST (EStree). 23 | 24 | The compiler and language core will have no external dependencies, and require 25 | no compilation. 26 | 27 | ## Consequences 28 | 29 | The compiler and language runtime can be used directly, from source, in any 30 | compliant JavaScript runtime. We only emit an AST, an external package will be 31 | needed to emit code. A number of EStree-based code generators are available 32 | off the shelf. 33 | -------------------------------------------------------------------------------- /doc/Piglet_Manual.adoc: -------------------------------------------------------------------------------- 1 | = Piglet, the Manual 2 | 3 | == The First Rule of Piglet Club 4 | 5 | Hello, stranger! I'm not sure how you got here, but you're here now. You may 6 | have a lot of questions, like "what am I even looking at"? 7 | 8 | == The Basics 9 | 10 | Piglet is a LISP environment written in EcmaScript, which can run in any 11 | EcmaScript compliant runtime. Our main reference are the most recent releases of 12 | popular browsers (Firefox, Safari, Chrome), and of Node.js. 13 | 14 | Piglet is heavily inspired by Clojure, so if you already know Clojure you're 15 | halfway there. Here are some of the main differences. 16 | 17 | === Differences from Clojure/ClojureScript 18 | 19 | ==== Dict 20 | 21 | What Clojure calls a (hash- or array-) map, Piglet calls a Dict (short for 22 | Dictionary). You can create one with a dict literal (curly braces) or with the 23 | dict function. 24 | 25 | [source,piglet] 26 | ------------------- 27 | {:size "L" :contents "green tea"} 28 | (type {}) ;;=> function Dict() {...} 29 | (dict :wake-time "10:00") 30 | ------------------- 31 | 32 | ==== Keyword/PrefixName/QName 33 | 34 | Keywords in Piglet are similar to Clojure Keywords, but we don't have namespaced 35 | keywords. You can use a slash (or multiple slashes), they have no special 36 | meaning. As in Clojure it is very common to use keywords as dict keys. 37 | 38 | [source,piglet] 39 | ------------------- 40 | :wood-grain 41 | :widget42 42 | :my/fancy/keyword 43 | 44 | (def m {:shoes 12}) 45 | 46 | (:shoes m) ;;=> 12 47 | ------------------- 48 | 49 | You get a QName or fully qualified name by writing a `:` followed by a URI (it 50 | has to contain `://`) 51 | 52 | [source,piglet] 53 | ------------------- 54 | :http://www.w3.org/2000/01/rdf-schema#comment 55 | ------------------- 56 | 57 | Functionally these behave like keywords 58 | 59 | [source,piglet] 60 | ------------------- 61 | (:http://www.w3.org/2000/01/rdf-schema#comment 62 | {:http://www.w3.org/2000/01/rdf-schema#comment "This is an interestings document"}) 63 | ;;=> "This is an interestings document" 64 | ------------------- 65 | 66 | The idea here is to make RDF-style identifiers first class. These are well 67 | defined identifiers defined by an authority, i.e. the person who controls the 68 | domain name. Often (but not always) you can open the URI as a URL in your 69 | browser to find out more about its meaning. 70 | 71 | But typing out these complete URIs is anything but convenient, so Piglet allows 72 | for a shorthand, for instance the above QName can be written as 73 | 74 | [source,piglet] 75 | ------------------- 76 | :rdfs:comment 77 | ------------------- 78 | 79 | This looks like a Keyword, but the additional `:` makes this a `PrefixName`, 80 | which during compilation gets converted to a full `QName`. It's really just 81 | shorthand, in other words, it's functionally equivalent to writing out the full 82 | `QName`. 83 | 84 | The expansion is determined by the current "context", a mapping from prefixes to 85 | URIs. Piglet ships with a number of default prefixes built in, like `rdf`, 86 | `rdfs`, `owl`, `dc`, or `foaf`, but you can set up your own in a module 87 | declaration or (to be implemented) package specification. 88 | 89 | Note that when Piglet prints a QName, it will also check the context, and if it 90 | finds a suitable prefix it will print the shorter PrefixName version, rather 91 | than the full QName, so in day to day usage you rarely ever encounter the full 92 | QName form. 93 | 94 | [source,piglet] 95 | ------------------- 96 | (type :rdfs:comment) ;;=> QName 97 | (type ':rdfs:comment) ;;=> PrefixName 98 | (type (read-string ":rdfs:comment") ;;=> PrefixName 99 | 100 | *qname-print-style* ;;=> :compact 101 | (print-str :foaf:name) ;;=> :foaf:name 102 | 103 | (set! *qname-print-style* :full) 104 | (print-str :foaf:name) ;;=> ":http://xmlns.com/foaf/0.1/name" 105 | ------------------- 106 | 107 | Here's a more real-world example: 108 | 109 | [source,piglet] 110 | ------------------- 111 | (def comment 112 | {:rdf:type :rdf:Property 113 | :rdfs:comment "A description of the subject resource." 114 | :rdfs:domain :rdfs:Resource 115 | :rdfs:isDefinedBy "http://www.w3.org/2000/01/rdf-schema#" 116 | :rdfs:label "comment" 117 | :rdfs:range :rdfs:Literal}) 118 | ------------------- 119 | 120 | The `name` function can give you a string version of any of these different 121 | identifiers, e.g. when you need to write them to a file or send them over the 122 | wire. This will not include any leading `:`. 123 | 124 | If you leave off the prefix, then you get an identifier based on the current 125 | package name. 126 | 127 | [source,piglet] 128 | ------------------- 129 | (name ::widget) ;;=> "https://piglet-lang.org/pkg/localpkg/widget" 130 | ------------------- 131 | 132 | -------------------------------------------------------------------------------- /doc/bookmarklet.html: -------------------------------------------------------------------------------- 1 | PDP Bookmarklet 2 | -------------------------------------------------------------------------------- /doc/built_in_protocols.md: -------------------------------------------------------------------------------- 1 | # Built-in Protocols 2 | 3 | ## Associative 4 | 5 | The `assoc` and `dissoc` functions make use of the `Associative` protocol, for 6 | functional updates (often just copy-on-write) of various associative data 7 | structures. `Associative` is implemented for: 8 | 9 | Piglet: 10 | - `dict` 11 | - `set` 12 | 13 | JavaScript: 14 | - `null` 15 | - `js:Array` (indexed access) 16 | - `js:Object` 17 | - `js:Set` 18 | 19 | ## Conjable 20 | 21 | The `conj` function make use of the `Conjable` protocol, for functionally adding 22 | one or more elements to a collection. `Conjable` is implemented for: 23 | 24 | Piglet: 25 | - `dict` 26 | - `set` 27 | - `cons` 28 | - `list` 29 | - `Context` 30 | - `Range` 31 | - `Repeat` 32 | 33 | JavaScript: 34 | - `null` 35 | - `js:Array` 36 | - `js:Object` 37 | - `js:Set` 38 | - `js:Map` 39 | 40 | ## Counted 41 | 42 | Note that more things than these listed can be counted with `count`, but they 43 | require walking a sequence. 44 | 45 | Functions: 46 | - `count` 47 | 48 | Piglet: 49 | - `list` 50 | - `dict` 51 | - `set` 52 | - `repeat` 53 | - `range` 54 | 55 | JavaScript: 56 | - `js:Array` 57 | - `js:Map` 58 | - `js:Set` 59 | 60 | ## Derefable 61 | 62 | Functions 63 | - `deref`, `@` 64 | 65 | - `Var` 66 | - `Box` 67 | 68 | ## DictLike 69 | 70 | ## Empty 71 | 72 | ## Eq 73 | 74 | ## Hashable 75 | 76 | ## Lookup 77 | 78 | ## MutableAssociative 79 | 80 | ## MutableCollection 81 | 82 | ## Named 83 | 84 | ## Repr 85 | 86 | ## Seq 87 | 88 | ## Seqable 89 | 90 | ## Sequential 91 | 92 | ## WithMeta 93 | 94 | ## Walkable 95 | 96 | ## QualifiedName 97 | 98 | ## Watchable 99 | 100 | ## Swappable 101 | 102 | ## TaggedValue 103 | 104 | 105 | -------------------------------------------------------------------------------- /doc/data_types.md: -------------------------------------------------------------------------------- 1 | # Piglet Data Types (WIP) 2 | 3 | Reference documentation for JS types and how they are used in Piglet, as well as 4 | the data types that Piglet itself brings to the table. 5 | 6 | ## Primitives 7 | 8 | ### nil 9 | 10 | The JS special value `null`, typically used to indicate the absence of a result 11 | or value, is represented in Piglet as `nil`. 12 | 13 | Piglet supports extending protocols to `nil`, and this is used extensively to 14 | provide "nil-punning", meaning `nil` can in many operations stand in for an 15 | empty list or empty dict. 16 | 17 | ```piglet 18 | (conj nil 1 2 3) => (3 2 1) 19 | (assoc nil :sound "oink") => {:sound "oink"} 20 | ``` 21 | 22 | ### undefined 23 | 24 | The JS special value `undefined` is available, but its use is discouraged. 25 | Generally Piglet built-ins will only ever return a value or `nil`, but not 26 | undefined. 27 | 28 | ### Boolean 29 | 30 | `true` and `false` are used to indicate boolean truth and falsehood. Note that 31 | Piglet's notion of truthyness differs from JavaScript. Only `false` and `nil` 32 | (and, should you encounter it, `undefined`) are considered falsy, everything 33 | else including `0` and the empty string, are considered truthy. 34 | 35 | -------------------------------------------------------------------------------- /doc/differences_from_clojure.md: -------------------------------------------------------------------------------- 1 | # Ways in Which Piglet Differs From Clojure 2 | 3 | Piglet has a lot of similarities with Clojure, but also numerous differences. We 4 | try to list the main ones here so people already familiar with Clojure can 5 | quickly get up to speed. 6 | 7 | - We use `:` instead of `/` as a separator in symbols and keywords 8 | - The slash is just a regular character 9 | 10 | - the map data structure is called dict 11 | 12 | ```piglet 13 | (= {:hello "world"} 14 | (dict :hello "world")) 15 | (instance? Dict o) 16 | (dict? o) 17 | ``` 18 | 19 | - split / join are part of the core API, and both take the separator first (for 20 | easy partial'ing) 21 | 22 | ```piglet 23 | (def s (partial split ",")) 24 | (def j (partial join "/")) 25 | 26 | (j (s "a,b,c")) 27 | => "a/b/c" 28 | ``` 29 | 30 | - Code is organized in packages, modules, and vars. Vars are like Clojure vars, 31 | modules are like namespaces, packages don't have a Clojure equivalent. 32 | 33 | -------------------------------------------------------------------------------- /doc/javascript_interop.md: -------------------------------------------------------------------------------- 1 | # JavaScript integration 2 | 3 | Piglet tries its best to make modern JavaScript features conveniently available. 4 | 5 | ## Literals 6 | 7 | The `#js` reader dispatch causes the compiler to emit JavaScript literals, 8 | either Arrays or Objects. 9 | 10 | ```lisp 11 | #js [1 2 3] 12 | #js {:foo 1 :bar 2} 13 | ``` 14 | 15 | Note that `#js` is not recursive, if you need nested JS arrays/objects, you need 16 | to prefix each of them. Otherwise you'll end up with Piglet datasctructures 17 | (Vector, Dict) inside your JS Arrays/Objects. 18 | 19 | ```lisp 20 | #js [#js {:sound "Oink"}] 21 | ``` 22 | 23 | ## Calling functions and looking up properties 24 | 25 | The `js:` prefix is considered special, symbols with this prefix are converted 26 | directly to JavaScript names. 27 | 28 | ```lisp 29 | (js:String "hello") 30 | ``` 31 | 32 | You can chain property lookups directly onto this: 33 | 34 | ```lisp 35 | (js:console.log "hello") 36 | (js:window.document.createElement "div") 37 | ``` 38 | 39 | A symbol starting with a period is converted to a method call: 40 | 41 | ```lisp 42 | (.appendChild el child) 43 | ``` 44 | 45 | A symbol starting with `.-` does a property lookup, rather than a function 46 | invocation. 47 | 48 | ```lisp 49 | (.-oink #js {:oink "OINK"}) 50 | (.-length "xxx") 51 | ``` 52 | 53 | Note that this is a low-level way of accessing properties, which will always do 54 | a simple JS object property lookup. Generally you can look up properties in JS 55 | objects the same way you do in Piglet dictionaries. 56 | 57 | ```lisp 58 | (:oink #js {:oink "OINK"}) 59 | (get {:oink "OINK"} :oink) 60 | ``` 61 | 62 | ## Manipulating data 63 | 64 | Generally working with JS collections (arrays and objects, Set and Map 65 | instances), you can simply use the same functions you use with Piglet 66 | collections like dictionaries and lists. 67 | 68 | - `conj` 69 | - `assoc` 70 | - `dissoc` 71 | - `into` 72 | - `get` 73 | - `get-in` 74 | - `assoc-in` 75 | - `update` 76 | - `update-in` 77 | 78 | But note that these are all pure functions, they don't change the collection you 79 | pass them, but instead create a new one. In other words: they need to copy over 80 | the entire collection each time you call them. This means that update and insert 81 | operations will get slower the more elements there are in the collection. This 82 | isn't so much a concern with Piglet's data structures since those are designed 83 | so we don't need to copy the entire collection (or at least they will be once we 84 | get around to it). 85 | 86 | We also have versions of these functions that do change the collection 87 | "in-place". 88 | 89 | - `conj!` 90 | - `assoc!` 91 | - `dissoc!` 92 | - `into!` 93 | - `assoc-in!` 94 | - `update!` 95 | - `update-in!` 96 | 97 | ```lisp 98 | (let [x #js {}] 99 | (assoc-in! x [:x :y] 1) 100 | (update-in! x [:x :y] inc) 101 | x) 102 | => #js {:x #js {:y 2}} 103 | ``` 104 | 105 | ## Protocol support 106 | 107 | JavaScript built-ins like Arrays, Objects, but also Map instances, ArrayBuffer, 108 | Uint8/32/64Array, etc. all implement one or more Piglet protocols, which means 109 | they can be used directly with many of Piglet's core functions. 110 | 111 | At a minimum these all implement Seqable, which means you can generally treat 112 | them as a sequential collection. So you can call functions like `first` or `map` 113 | on them, and they can take part in destructuring. 114 | 115 | Arrays can be destructured. 116 | 117 | ```lisp 118 | (let [[x y] #js [1 2]] (+ x y)) 119 | (map (fn [[k v]] (str k "--" v)) #js {:foo 1}) 120 | => ("foo--1") 121 | ``` 122 | 123 | Objects can also be destructured. 124 | 125 | ```lisp 126 | (let [{foo :foo} #js {:foo 123}] 127 | foo) 128 | ``` 129 | 130 | This compiles to a `piglet:get` lookup, which checks if the object implements 131 | the `Lookup` protocol (e.g. it's a `Dict`, `js:Map`, `js:Set`, `Module`). If 132 | not, then it does a plain object key lookup. The same is true for `:keys`, 133 | `:strs`, or `:syms`, which will all compile to a call to `(piglet:get object 134 | string-or-keyword-or-symbol)`. 135 | 136 | To compile to direct property access, regardless of the type of object or the 137 | protocols it implements, use `:props` 138 | 139 | ```piglet 140 | (let [{:keys [foo] 141 | :props [bar]} #js {:foo 123 :bar 456}] 142 | [foo bar]) 143 | ``` 144 | 145 | ```js 146 | const dict_as42 = { 147 | "foo": 123, 148 | "bar": 456 149 | }; 150 | const foo7 = $piglet$["https://piglet-lang.org/packages/piglet"].lang.get(dict_as42, $piglet$["https://piglet-lang.org/packages/piglet"].lang.keyword("foo"), null); 151 | const bar4 = dict_as42.bar; 152 | ``` 153 | 154 | Conversely, all Piglet datastructures implement Symbol.iterator. So you can 155 | generally use them in JavaScript functions that take some kind of iterable 156 | collection. 157 | 158 | ```lisp 159 | (js:Map. {"hello" "world"}) 160 | 161 | (js:Array.from (range 5)) 162 | ;; #js [0, 1, 2, 3, 4] 163 | ``` 164 | 165 | See [Built-in Protocols](built_in_protocols.md) for more details on the various protocols. 166 | 167 | ## Working with promises 168 | 169 | Functions can be marked as async, and there's an await special form 170 | 171 | ```lisp 172 | (defn ^:async my-fn [] 173 | (await (js:fetch "..."))) 174 | ``` 175 | 176 | For loops also support `^:async`, the result is a promise to a seq. 177 | 178 | ```lisp 179 | (for ^:async [url ["http://example.com"]] 180 | (js:fetch url)) 181 | ``` 182 | 183 | ## Loading JS modules 184 | 185 | You can load JavaScript modules/packages by using strings in your module's 186 | import form. 187 | 188 | ```lisp 189 | (module my-first-module 190 | (:import 191 | [lp :from "leftpad"] 192 | [g :from "glob")) 193 | ``` 194 | 195 | This does a dynamic `await import("leftpad")`. It then creates a Piglet Module 196 | aliased to the given alias, and interns any exported values into that module. If 197 | there's a `default` export. A var is also created with the alias name in the 198 | current module. If the imported module has a default export, then that is used, 199 | otherwise the var points at the Module itself. 200 | 201 | In the example above, `leftpad` has a default export, while `glob` does not. 202 | 203 | ```lisp 204 | (lp:default "xx" 3) ; 'default' var in the 'lp' module 205 | (lp "xx" 3) ; 'lp' var in the current module, same thing 206 | 207 | (g:globSync "*.pig") ; exported function from "glob" 208 | 209 | ;; It's a first class piglet var in a synthetic `js-interop/glob` module, aliased to `g` 210 | #'g:globSync ;;=> #'js-interop/glob:globSync 211 | 212 | (keys g) ; Modules implement DictLike 213 | ;;=> #js [__raw__, Glob, Ignore, escape, glob, globIterate, globIterateSync, globStream, globStreamSync, globSync, hasMagic, iterate, iterateSync, stream, streamSync, sync, unescape] 214 | 215 | g:__raw__ ; the raw JS object we got back from `await import(,,,)` (subject to change, may not be available when AOT compiling) 216 | ``` 217 | 218 | On Node.js we mostly mimic Node's own package resolution rules. That means that 219 | if you `npm install foo` and then `(:import [foo :from "foo"])` it generally 220 | just works. 221 | 222 | As in Node itself to import built-in packages, use the `node:` prefix. E.g. 223 | `[process :from "node:process"]`. 224 | 225 | In the browser you can add an import-map to tell the browser how to resolve 226 | these files. 227 | 228 | ```html 229 | 234 | ``` 235 | 236 | -------------------------------------------------------------------------------- /doc/modules_and_packages.md: -------------------------------------------------------------------------------- 1 | # Modules and Packages 2 | 3 | Each Piglet (`.pig`) file defines a Piglet Module, which compiles to a 4 | JavaScript module (`.mjs`). 5 | 6 | Modules are organized in Packages, which is the unit of code distribution. At 7 | the project root you should have a `package.pig` file which defines your 8 | project's package, which directories contain source files, and which Piglet 9 | dependencies your package has. 10 | 11 | ```lisp 12 | {:pkg:id "https://your.org/piglet-packages/my-package" 13 | :pkg:path ["src"] 14 | :pkg:deps {some-alias {:pkg:location "../other-package-dir"}}} 15 | ``` 16 | 17 | Module files within a package are placed at `src/.pig`, so 18 | `my-first-module` is placed at `src/my-first-module.pig`, and the module 19 | `util/data` is placed in `src/util/data.pig`. Notice that no munging happens on 20 | the module name, the file names mimic the module names exactly, including 21 | underscores and slashes. 22 | 23 | The first form in the module file should be the module declaration. 24 | 25 | ```lisp 26 | (module my-first-module 27 | (:import 28 | [data :from util/data] ; from current package, explicit alias 29 | helpers ; from current package, name = alias 30 | [dom :from piglet:dom] ; from built-in piglet package 31 | [other-mod :from some-alias:other-mod] ; from dependent package, some-alias defined in package.pig 32 | [lp :from "leftpad"])) ; JS import, gets turned into a module + vars 33 | ``` 34 | 35 | This demonstrates the options you have when importing. 36 | 37 | - `[data :from util/data]` - since `util/data` is an unqualified symbol (no `:`) 38 | this is understood as referring to a module in the same package as this 39 | module. The vars in `util/data.pig` are available in this module with the 40 | `data:` prefix, e.g. `(data:remove-entity-items ,,,)` 41 | - `helpers` - a bare identifier like this is equivalent to `[helpers :from 42 | helpers]`, this gives you an easy way to depend on modules in the same package 43 | - `[dom :from piglet:dom]` - This loads the `dom` module inside the built-in 44 | `piglet` package. The `piglet` alias is always available. 45 | - `[other-mod :from some-alias:other-mod]` - Load from a different package, 46 | `some-alias` is the package alias defined in `package.pig` of this package. 47 | - `[lp :from "leftpad"]` - Strings get converted to JS import statements, and 48 | the exports therein are turned into piglet vars. 49 | 50 | For more info on importing and using JS modules, see [JavaScript Interop](javascript_interop.md). 51 | 52 | -------------------------------------------------------------------------------- /doc/piglet_overview.md: -------------------------------------------------------------------------------- 1 | # Piglet Language Overview 2 | 3 | ## Keywords and Symbols, QNames and QSyms 4 | 5 | Piglet has plain unqualified symbols, which evaluate to local variables or var 6 | lookups, like any other LISP. It also has plain keywords, which start with a 7 | colon, and are simple interned identifiers that evaluate to themselves, like 8 | unqualified keywords in Clojure. As in Clojure these behave like functions which 9 | perform a keyword lookup (works both for Piglet data structures and plain JS 10 | maps). 11 | 12 | Clojure also has qualified or namespaced symbols and keywords. The former as a 13 | way to identify a var within a namespace, the latter as a fully qualified 14 | identifier, allowing keys in dictionary-like objects to be less prone to 15 | collision, and able to carry precise semantics for the associated value. Both 16 | symbols and keywords can be abbreviated based on namespace aliases for 17 | terseness. 18 | 19 | One of the ideas we wanted to explore with Piglet is: what if Clojure had went 20 | all the way and used full URIs as identifiers, both for data, and for code (var 21 | resolution)? The result are `QName`s as an alternative to qualified keywords, 22 | and `QSym`s as an alternative to qualified symbols. 23 | 24 | ## QName 25 | 26 | A `QName` is a interned absolute URI identifier intended for data 27 | representation. Written in their full form as syntax literals they start with a 28 | colon, and must contain `://`. 29 | 30 | ```lisp 31 | :http://xmlns.com/foaf/0.1/name 32 | ``` 33 | 34 | Fully writing them out like this however is unwieldy. Instead a set of prefixes 35 | can be configured, which will be understood by both the reader and the printer. 36 | A number of common default prefixes are available, like `rdf`, `owl`, `foaf`, or 37 | `svg`. 38 | 39 | ```lisp 40 | piglet:lang=> (fqn :foaf:name) 41 | "http://xmlns.com/foaf/0.1/name" 42 | ``` 43 | 44 | Both printing and reading are controlled by the `*current-context*` var, which 45 | holds a Dict from prefix to URI. If you've worked with JSON-LD this idea of a 46 | current context should be familiar. 47 | 48 | The idea is that both RDF identifiers and namespaced XML element names can be 49 | represented directly, and that truly globally unique identifiers become the 50 | norm, while at the source level one gets to largely ignore this verboseness 51 | thanks to alias prefixes. 52 | 53 | ```lisp 54 | piglet:lang=> (set! *current-context* (assoc *current-context* "foo" "https://example.com/foo#")) 55 | piglet:lang=> (fqn :foo:bar) 56 | "https://example.com/foo#bar" 57 | ``` 58 | 59 | Note that the separator here is the colon, as in JSON-LD, not the slash. Slashes 60 | hold no special meaning, and keywords (or symbols) can contain any number of 61 | slashes. 62 | 63 | ## QSyms, Packages, Modules, Vars 64 | 65 | Both on disk and in-memory Piglet code is organized in Packages, Modules, and 66 | Vars. Packages are fully first class, rather than being merely a boot-time 67 | concern that then all gets lumped together into a single search path. A package 68 | has a name, which is a URI (if no name is specified it gets a `file://` name 69 | based on its location on disk). A package maps to a set of source files. 70 | 71 | Each source file within a package defines a piglet Module (and can be compiled 72 | to a ES6 module). The module name follows the file name starting from the 73 | module's configured source directory (`src` by default). So a module `foo/bar` 74 | goes into `src/foo/bar.pig`. 75 | 76 | A module defines var (with `def` or `defn`), which gets interned into the 77 | Module, which in turn is part of the Package, which is stored in the 78 | ModuleRegistry. 79 | 80 | Vars have fully qualified names, which is the name of the package, module, and 81 | var, combined with colons. 82 | 83 | ``` 84 | piglet:lang=> (fqn #'assoc) 85 | https://piglet-lang.org/packages/piglet:lang:assoc 86 | ``` 87 | 88 | A package has a `package.pig` file at the package root. 89 | 90 | ```lisp 91 | {:pkg:name https://my-project.org/packages/hello-world 92 | :pkg:paths ["."] 93 | :pkg:deps {foolib {:pkg:location "../foolib"}}} 94 | ``` 95 | 96 | (`pkg` is a prefix in the default context aliased to `https://vocab.piglet-lang.org/package/`) 97 | 98 | The `:pkg:deps` is in itself a sort of alias declaration, in this case it's 99 | stating that within this `hello-world` package, the package which is located at 100 | `../foolib` is aliased to `foolib`. Now we can load a module from that package, 101 | assigning it its own local prefix. 102 | 103 | ```lisp 104 | (module foo/bar 105 | (:import [bar :from foolib:foo/bar])) 106 | ``` 107 | 108 | ## Standard library 109 | 110 | Some libraries that are bundled with Piglet include 111 | 112 | - piglet:string 113 | - piglet:dom 114 | - piglet:cbor 115 | 116 | -------------------------------------------------------------------------------- /doc/porting_clojure_code.md: -------------------------------------------------------------------------------- 1 | # Porting Clojure Code to Piglet 2 | 3 | An overview of the most salient differences you'll run into 4 | 5 | - Use `:` instead of `/` to separate module and var name, `str:split`, not `str/split` 6 | - There's no `next`, only `rest` 7 | - `contains?` is called `has-key?` (can be used on sets and dicts) 8 | - `str/lower-case` is `str:downcase` (same for `str:upcase`) 9 | - There are no character literals (like `\A` or `\newline`), use strings. E.g. `(= "a" (first "a"))` 10 | - The argument order of `str:split` is reversed, to match `str:join` and to be 11 | more amenable to partial function application 12 | - `deftype` works completely differently, and `defclass` is also not the same as 13 | the `defclass` provided by Shadow-cljs. See [Class Syntax](./class_syntax.md). 14 | - `class` creates a new JS class value (possibly anonymous), it follows the 15 | syntax of `defclass`. It does not return the class of an object. Use `type` or 16 | `type-name`. 17 | - maps are called dicts, hence `hash-map` / `array-map` is `dict`, `map?` is 18 | `dict?` 19 | 20 | -------------------------------------------------------------------------------- /doc/quickstart.md: -------------------------------------------------------------------------------- 1 | # Piglet quickstart 2 | 3 | Grab piglet from npm 4 | 5 | ``` 6 | npm install -g piglet-lang 7 | ``` 8 | 9 | This will install two executables, `piglet` and `pig`. You mainly will use 10 | `pig`. Use `piglet` in script shebang lines, e.g. `#!/usr/bin/env piglet`. 11 | 12 | ``` 13 | COMMAND 14 | pig —— Piglet's project tool 15 | 16 | USAGE 17 | pig [--verbose | -v] [repl | pdp | web | aot] [...] 18 | 19 | FLAGS 20 | -v, --verbose Increase verbosity 21 | 22 | SUBCOMMANDS 23 | repl Start a Piglet REPL 24 | pdp Connect to Piglet Dev Protocol server (i.e. your editor) 25 | web Start dev-server for web-based projects 26 | aot AOT compile the given module and its dependencies 27 | ``` 28 | 29 | - `pig repl` start a node-based REPL in the terminal 30 | - `pig pdp` is for interactive programming (see later) 31 | - `pig web` starts a web server for using piglet in the browser 32 | - `pig aot` compiles a module in your project (experimental) 33 | 34 | The root of a piglet project should contain a `package.pig` 35 | 36 | ``` 37 | {:pkg:name https://my.org/piglet-packages/my-pkg 38 | :pkg:paths ["src"]} 39 | ``` 40 | 41 | Piglet module names follow the file system starting from `:pkg:paths`, create 42 | `src/my/first/module.pig` 43 | 44 | ```lisp 45 | (module my/first/module 46 | (:import 47 | [t :from piglet:cli/terminal])) 48 | 49 | (println (t:fg :magenta "Cooking with gas!")) 50 | ``` 51 | 52 | Run it with 53 | 54 | ``` 55 | bin/pig run my/first/module 56 | ``` 57 | 58 | ## Emacs 59 | 60 | The [piglet-emacs](https://github.com/piglet-lang/piglet-emacs) package contains 61 | `piglet-mode.el`, a treesitter-based mode with indentation and syntax 62 | highlighting, `pdp.el`, which implements the server side of the Piglet Dev 63 | Protocol, used for interactive programming, and `piglet-company.el`, which uses 64 | PDP to provide completion candidates to company-mode. 65 | 66 | Piglet-emacs is not available from MELPA. If you're using the Straight 67 | functional package manager for emacs, then you get piglet-emacs via the 68 | corgi-packages repository. 69 | 70 | ```lisp 71 | (use-package corgi-packages 72 | :straight (corgi-packages 73 | :type git 74 | :host github 75 | :repo "corgi-emacs/corgi-packages")) 76 | 77 | (add-to-list #'straight-recipe-repositories 'corgi-packages) 78 | 79 | (use-package piglet-emacs) 80 | ``` 81 | 82 | Or just clone piglet-emacs and load the provided `.el` files manually. See 83 | `piglet-emacs-pkg.el` for a list of dependencies that it needs. These are all 84 | available from MELPA. 85 | 86 | ## PDP 87 | 88 | To provide interactive programming facilities Piglet uses a message based 89 | protocol over websockets, using CBOR as the over-the-wire format. This is known 90 | as Piglet Dev Protocol or PDP. 91 | 92 | In the PDP model your editor is the server, and the Piglet runtime connects to 93 | it on a well known port (17017). It's done this way and not the other way around 94 | so that we can use the same mechanism from the browser as well as from other 95 | runtimes like node.js. 96 | 97 | First, in emacs, start the PDP server, `M-x pdp-start-server!`. 98 | 99 | Then, start a piglet runtime in your project, connecting to PDP: `pig pdp` 100 | 101 | You'll see a message in your minibuffer: 102 | 103 | ``` 104 | [Piglet] PDP conn opened, 1 active connections 105 | ``` 106 | 107 | PDP offers a slew of `eval` variants, determining what is being evaluated, and 108 | where the result is displayed. 109 | 110 | ``` 111 | pdp-eval-{last-sexp,outer-sexp,buffer,region}-to-{minibuffer,insert,result-buffer,repl} 112 | ``` 113 | 114 | For instance `eval-last-sexp-to-insert`, `pdp-eval-region-to-result-buffer`, etc. 115 | 116 | Note: all of these also have a `...-pretty-print` variant, support for pretty 117 | printing however is not yet implemented on the client. 118 | 119 | ## Web 120 | 121 | To use piglet in a browser, you can use `pig web` in your project. This will 122 | start a web server on port 1234, which when accessed will load the piglet 123 | compiler, the PDP client, and your main module (if you have a `:pkg:main` in 124 | your `package.pig`). From there you can eval and develop in the same way as you 125 | would do on node.js. 126 | 127 | ## AOT 128 | 129 | AOT or ahead-of-time compilation refers to transpiling Piglet modules to ES6 130 | modules. These can then be further bundled using esbuild, rollup, vite, etc. 131 | 132 | This is currently mainly a proof of concept. The main difference between how 133 | code is generated in AOT mode vs when loading Piglet code at runtime or 134 | evaluating it in the REPL, is that AOT compilation compiles Piglet vars to JS 135 | variables, and uses ES6 import/export statements, instead of referencing a 136 | global module registry. This opens the door to tree shaking (dead code 137 | elimination). Note that much of Piglet's supporting code written in JS currently 138 | does not tree shake very well. 139 | 140 | To try out AOT compilation and see what the result looks like, you can use `pdp 141 | aot `. The result is written to the `target/` directory. 142 | -------------------------------------------------------------------------------- /lib/piglet/browser/BrowserCompiler.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import * as astring from "astring" 4 | 5 | import AbstractCompiler from "../lang/AbstractCompiler.mjs" 6 | import {module_registry, resolve, deref, symbol, read_string, expand_qnames} from "../lang.mjs" 7 | import {assert} from "../lang/util.mjs" 8 | 9 | export default class BrowserCompiler extends AbstractCompiler { 10 | async slurp(path) { 11 | const response = await fetch(path) 12 | return await response.text() 13 | } 14 | 15 | async slurp_mod(pkg_name, mod_name) { 16 | const pkg = module_registry.find_package(pkg_name) 17 | for (const dir of pkg.paths) { 18 | const location = new URL(`${mod_name}.pig`, new URL(`${dir}/`, new URL(pkg.location, window.location))).href 19 | const response = await fetch(location) 20 | if (response.ok) return [await response.text(), location] 21 | } 22 | } 23 | 24 | resolve_js_path(js_path) { 25 | const mod = deref(resolve(symbol("piglet:lang:*current-module*"))) 26 | 27 | if (js_path.startsWith("./") || js_path.startsWith("../") || js_path.startsWith("/")) { 28 | return new URL(js_path, mod.location).href 29 | } 30 | return js_path 31 | } 32 | 33 | estree_to_js(estree) { 34 | if (window.sourceMap) { 35 | const mod = deref(resolve(symbol("piglet:lang:*current-module*"))) 36 | if (mod.location) { 37 | try { 38 | const map = new sourceMap.SourceMapGenerator({ 39 | file: mod.location, skipValidation: true 40 | }) 41 | const code = astring.generate(estree, {sourceMap: map}) 42 | return `${code}\n//# sourceURL=${mod.location}?line=${estree.line}\n//# sourceMappingURL=data:application/json;base64,${btoa(map.toString())}` 43 | } catch (e) { 44 | console.error("ERROR: astring failed on input", estree) 45 | throw(e) 46 | } 47 | } else { 48 | console.warn(`WARN: ${mod.inspect()} misses location, no source map generated.`) 49 | } 50 | } 51 | return astring.generate(estree) 52 | } 53 | 54 | async load_package(location) { 55 | assert(location) 56 | const package_pig_loc = new URL("package.pig", new URL(`${location}/`, window.location)).href 57 | const response = await fetch(package_pig_loc) 58 | if (response.ok) { 59 | let package_pig = expand_qnames(read_string(await response.text())) 60 | return this.register_package(package_pig_loc, package_pig) 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /lib/piglet/browser/main.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import BrowserCompiler from "./BrowserCompiler.mjs" 4 | 5 | import {intern, qsym, symbol, module_registry, prefix_name} from "../lang.mjs" 6 | window.$piglet$ = module_registry.index 7 | 8 | const verbosity_str = new URL(import.meta.url).searchParams.get("verbosity") 9 | const compiler = new BrowserCompiler({}) 10 | intern(symbol("piglet:lang:*compiler*"), compiler) 11 | intern(symbol("piglet:lang:*verbosity*"), verbosity_str ? parseInt(verbosity_str, 10) : 0) 12 | 13 | // defined as a constant so it's easy to replace when using a build system 14 | const PIGLET_PACKAGE_PATH = new URL("../../../packages/piglet", import.meta.url) 15 | await compiler.load_package(PIGLET_PACKAGE_PATH) 16 | 17 | compiler.load(symbol("piglet:lang")).then(()=>{ 18 | for (const script of document.getElementsByTagName("script")) { 19 | if(script.type == 'application/piglet' || script.type == 'piglet') { 20 | if (script.src) { 21 | compiler.load_file(script.src) 22 | } else { 23 | compiler.eval_string(script.innerText) 24 | } 25 | } 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /lib/piglet/lang/AbstractIdentifier.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Lookup from "./protocols/Lookup.mjs" 4 | import {fixed_prop} from "./util.mjs" 5 | import {assert} from "./util.mjs" 6 | import {meta, set_meta} from "./metadata.mjs" 7 | 8 | /** 9 | * Base class for Sym, QSym, Keyword, QName, PrefixName. 10 | * 11 | * Contains some dark magic to ensure these are callable as if they are 12 | * functions (which in JS land means they have to _be_ functions), hence the 13 | * constructor which returns a function which delegates to the Lookup protocol. 14 | * We then redefine the prototype so that they also behave as instances of their 15 | * respective classes as defined. The magic is further darkened by the fact that 16 | * we want the function name to match the identifier name. 17 | */ 18 | export default class AbstractIdentifier { 19 | constructor(meta, sigil, name, id_str) { 20 | assert(id_str) 21 | const self = name ? ({[name](coll, fallback) { 22 | if (Lookup.satisfied(coll)) { 23 | if (fallback === undefined) { 24 | return Lookup._get(coll, self) 25 | } 26 | return Lookup._get(coll, self, fallback) 27 | } 28 | fallback = fallback === undefined ? null : fallback 29 | if (coll != null) { 30 | const n = self.fqn || self.name 31 | return n in coll ? coll[n] : fallback 32 | } 33 | return fallback 34 | }}[name]) : (function (coll, fallback) { 35 | if (Lookup.satisfied(coll)) { 36 | if (fallback === undefined) { 37 | return Lookup._get(coll, self) 38 | } 39 | return Lookup._get(coll, self, fallback) 40 | } 41 | fallback = fallback === undefined ? null : fallback 42 | if (coll != null) { 43 | const n = self.fqn || self.name 44 | return n in coll ? coll[n] : fallback 45 | } 46 | return fallback 47 | }) 48 | 49 | Object.setPrototypeOf(self, this.constructor.prototype) 50 | fixed_prop(self, "_sigil", sigil) 51 | fixed_prop(self, "_id_str", id_str) 52 | set_meta(self, meta) 53 | return self 54 | } 55 | 56 | identifier_str() { 57 | return this._id_str 58 | } 59 | 60 | toString() { 61 | return `${this._sigil}${this._id_str}` 62 | } 63 | 64 | inspect() { 65 | return `${this.constructor.name}(${this.toString()})` 66 | } 67 | 68 | [Symbol.for('nodejs.util.inspect.custom')](depth, options, inspect) { 69 | return `${options.stylize(this.constructor.name, 'special')}(${options.stylize(this.toString(), 'symbol')})` 70 | } 71 | } 72 | 73 | Object.setPrototypeOf(AbstractIdentifier.prototype, Function) 74 | -------------------------------------------------------------------------------- /lib/piglet/lang/AbstractSeq.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023-2025. All rights reserved. 2 | 3 | export default class AbstractSeq { 4 | /** 5 | * Return the first value in the Seq, or `null` if empty 6 | */ 7 | first() { 8 | throw new Error("Not implemented") 9 | } 10 | 11 | /** 12 | * Return a Seq of the remaining values beyond the first. Returns `null` if 13 | * there are no remaining elements. 14 | */ 15 | rest() { 16 | throw new Error("Not implemented") 17 | } 18 | 19 | /** 20 | * Return `this`, or `null` if the Seq is empty. This allows us to 21 | * distinguish between `(nil)` and `()` 22 | */ 23 | seq() { 24 | throw new Error("Not implemented") 25 | } 26 | 27 | empty() { 28 | return this.seq() === null 29 | } 30 | 31 | [Symbol.iterator]() { 32 | let it = this 33 | return {next: ()=>{ 34 | const v = it.first() 35 | it = it.next() 36 | if (!it) { 37 | return {done: true} 38 | } 39 | return {value: v} 40 | }} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/piglet/lang/Cons.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import AbstractSeq from "./AbstractSeq.mjs" 4 | import Empty from "./protocols/Empty.mjs" 5 | import {set_meta} from "./metadata.mjs" 6 | 7 | export default class Cons extends AbstractSeq { 8 | constructor(x, xs) { 9 | super() 10 | this.x=x 11 | this.xs=xs 12 | } 13 | first() { 14 | return this.x 15 | } 16 | rest() { 17 | return Empty.satisfied(this.xs) && Empty._empty_$QMARK$_(this.xs) ? 18 | null : this.xs 19 | } 20 | seq() { 21 | return this 22 | } 23 | with_meta(m) { 24 | return set_meta(new Cons(this.x, this.xs), m) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/piglet/lang/Context.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import QName from "./QName.mjs" 4 | import PrefixName from "./PrefixName.mjs" 5 | import {keyword} from "./Keyword.mjs" 6 | import {assert} from "./util.mjs" 7 | import Lookup from "./protocols/Lookup.mjs" 8 | import Dict from "./Dict.mjs" 9 | import {meta} from "./metadata.mjs" 10 | 11 | export const default_prefixes = Dict.of_pairs(null, Object.entries({ 12 | pkg: "https://vocab.piglet-lang.org/package/", 13 | dc: "http://purl.org/dc/elements/1.1/", 14 | dcterms: "http://purl.org/dc/terms/", 15 | foaf: "http://xmlns.com/foaf/0.1/", 16 | org: "http://www.w3.org/ns/org#", 17 | owl: "http://www.w3.org/2002/07/owl#", 18 | rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 19 | rdfs: "http://www.w3.org/2000/01/rdf-schema#", 20 | schema: "http://schema.org/", 21 | xsd: Dict.of_pairs(null, [[keyword("base"), "http://www.w3.org/2001/XMLSchema"], 22 | [keyword("separator"), "#"]]), 23 | svg: Dict.of_pairs(null, [[keyword("base"), "http://www.w3.org/2000/svg"], 24 | [keyword("separator"), "#"]]), 25 | xhtml: Dict.of_pairs(null, [[keyword("base"), "http://www.w3.org/1999/xhtml"], 26 | [keyword("separator"), "#"]]) 27 | })) 28 | 29 | export default class Context { 30 | constructor() { 31 | throw new Error("Context is not constructible") 32 | } 33 | 34 | static expand(ctx, prefix_name) { 35 | let base = prefix_name.prefix || "self" 36 | let separator = "" 37 | let expanded_prefix 38 | let suffix 39 | if (default_prefixes.get(base)) { 40 | base = default_prefixes.get(base) 41 | } else { 42 | while (expanded_prefix = ctx.get(base instanceof Dict ? base.get(keyword("base")) : base)) { 43 | base = expanded_prefix 44 | } 45 | } 46 | [base, separator] = (base instanceof Dict ? 47 | [base.get(keyword("base")), base.get(keyword("separator"))] : 48 | [base, ""] 49 | ) 50 | assert(base.indexOf("://") !== -1, `PrefixName did not expand to full QName, missing prefix. ${prefix_name} in ${ctx}`) 51 | return new QName(meta(prefix_name), base, separator || "", prefix_name.suffix) 52 | } 53 | 54 | static contract(ctx, qname) { 55 | let prefix, suffix 56 | for (let [k,v] of default_prefixes) { 57 | if (v instanceof Dict) { 58 | v = v.get(keyword('base')) 59 | } 60 | if (qname.fqn.startsWith(v)) { 61 | prefix = k 62 | suffix = qname.fqn.slice(v.length) 63 | return new PrefixName(meta(qname), prefix == 'self' ? null : prefix, suffix) 64 | } 65 | } 66 | 67 | for (let [k, v] of ctx) { 68 | if (v instanceof Dict) { 69 | v = v.get(keyword('base')) 70 | } 71 | if (qname.fqn.startsWith(v)) { 72 | prefix = k 73 | suffix = qname.fqn.slice(v.length) 74 | return new PrefixName(meta(qname), prefix == 'self' ? null : prefix, suffix) 75 | } 76 | } 77 | 78 | return qname 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/piglet/lang/Dict.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Sym from "./Sym.mjs" 4 | import {partition_n} from "./util.mjs" 5 | import {meta, set_meta} from "./metadata.mjs" 6 | import {assert, fixed_prop} from "./util.mjs" 7 | import Eq from "./protocols/Eq.mjs" 8 | import Repr from "./protocols/Repr.mjs" 9 | import {keyword} from "./Keyword.mjs" 10 | 11 | /** 12 | * Naive copy-on-write dictionary backed by a frozen js Map. 13 | */ 14 | export default class Dict { 15 | constructor(metadata, m) { 16 | assert(m instanceof Map, "Dict entries has to be js:Map") 17 | const self = function(key, fallback) { 18 | if (self.has(key)) return self.get(key) 19 | return fallback === undefined ? null : fallback 20 | } 21 | Object.setPrototypeOf(self, this.constructor.prototype) 22 | fixed_prop(self, "entries", Object.freeze(m || new Map())) 23 | if (metadata) set_meta(self, metadata) 24 | return self 25 | } 26 | 27 | static of(meta, ...kvs) { 28 | const m = new Map() 29 | for (let [k, v] of partition_n(2, kvs)) { 30 | m.set(k, v) 31 | } 32 | return new this(meta, m) 33 | } 34 | 35 | static of_pairs(meta, kvs) { 36 | const m = new Map() 37 | for (let [k, v] of kvs) { 38 | m.set(k, v) 39 | } 40 | return new this(meta, m) 41 | } 42 | 43 | assoc(k, v) { 44 | if (this.has(k)) { 45 | return new Dict(meta(this), new Map(this.dissoc(k).entries).set(k, v)) 46 | } 47 | return new Dict(meta(this), new Map(this.entries).set(k, v)) 48 | } 49 | 50 | dissoc(k) { 51 | const entries = new Map(this.entries) 52 | if (this.entries.has(k)) { 53 | entries.delete(k) 54 | } else if (Eq.satisfied(k)) { 55 | for (const [kk, vv] of this.entries.entries()) { 56 | if (Eq._eq(k, kk)) { 57 | entries.delete(kk) 58 | } 59 | } 60 | } 61 | return new Dict(meta(this), entries) 62 | } 63 | 64 | with_meta(m) { 65 | return new Dict(m, this.entries) 66 | } 67 | 68 | [Symbol.iterator]() { 69 | return this.entries[Symbol.iterator]() 70 | } 71 | 72 | has(k) { 73 | if (this.entries.has(k)) { 74 | return true 75 | } 76 | if (Eq.satisfied(k)) { 77 | for (const [kk, vv] of this.entries.entries()) { 78 | if (Eq._eq(k, kk)) { 79 | return true 80 | } 81 | } 82 | } 83 | return false 84 | } 85 | 86 | get(k, fallback) { 87 | if (this.entries.has(k)) { 88 | return this.entries.get(k) 89 | } 90 | if (Eq.satisfied(k)) { 91 | for (const [kk, vv] of this.entries.entries()) { 92 | if (Eq._eq(k, kk)) { 93 | return vv 94 | } 95 | } 96 | } 97 | if (fallback === undefined) { 98 | return null 99 | } 100 | return fallback 101 | } 102 | 103 | // HACK: For internal use. We can't construct keyword instances inside 104 | // Protocol.mjs because it would lead to circular dependencies 105 | get_kw(name) { 106 | return this.get(keyword(name)) 107 | } 108 | 109 | keys() { 110 | return this.entries.keys() 111 | } 112 | 113 | values() { 114 | return this.entries.values() 115 | } 116 | 117 | count() { 118 | return this.entries.size 119 | } 120 | 121 | emit(cg) { 122 | return cg.invoke_var(this, 123 | "piglet", "lang", "dict", 124 | Array.from(this.entries).flatMap(([k, v])=>[cg.emit(this, k), cg.emit(this, v)])) 125 | } 126 | 127 | inspect() { 128 | let recur = (v)=> (typeof v === 'object' || typeof v === 'function') && v?.inspect ? v.inspect() : Repr.satisfied(v) ? Repr._repr(v) : v 129 | return `Dict(${Array.from(this, (([k, v])=>`${recur(k)} ${recur(v)}`)).join(", ")})` 130 | } 131 | 132 | toJSON() { 133 | return Object.fromEntries(Array.from(this, ([k, v]) => [k.identifier_str ? k.identifier_str() : k, v])) 134 | } 135 | 136 | [Symbol.for('nodejs.util.inspect.custom')](depth, options, inspect) { 137 | return inspect(this.entries, options).replace(/Map/, 'Dict') 138 | } 139 | } 140 | 141 | export const EMPTY = new Dict(null, new Map()) 142 | 143 | Dict.EMPTY = EMPTY 144 | 145 | export function dict(...args) { return Dict.of(null, ...args) } 146 | -------------------------------------------------------------------------------- /lib/piglet/lang/HashSet.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Sym from "./Sym.mjs" 4 | import {partition_n} from "./util.mjs" 5 | import {meta, set_meta} from "./metadata.mjs" 6 | import {assert, fixed_prop} from "./util.mjs" 7 | import {hash_code} from "./hashing.mjs" 8 | import Eq from "./protocols/Eq.mjs" 9 | import Repr from "./protocols/Repr.mjs" 10 | 11 | export {hash_code} 12 | 13 | function eq(thiz, that) { 14 | if (thiz === that) return true 15 | if (hash_code(thiz) !== hash_code(that)) return false 16 | if (Eq.satisfied(thiz)) return Eq._eq(thiz, that) 17 | return false 18 | } 19 | 20 | function arr_has(arr, o) { 21 | for (const a of arr) { 22 | if (eq(a, o)) return true 23 | } 24 | return false 25 | } 26 | 27 | function madd(m, o) { 28 | const hsh = hash_code(o) 29 | if (!m.has(hsh)) m.set(hsh, []) 30 | const bucket = m.get(hsh) 31 | if (!arr_has(bucket, o)) 32 | bucket.push(o) 33 | return m 34 | } 35 | 36 | function mremove(m, o) { 37 | const hsh = hash_code(o) 38 | if (m.has(hsh)) { 39 | m.set(hsh, m.get(hsh).filter((x)=>!eq(x,o))) 40 | } 41 | return m 42 | } 43 | 44 | function mhas(m, o) { 45 | const hsh = hash_code(o) 46 | if(m.has(hsh)) 47 | for (const x of m.get(hsh)) 48 | if (eq(x,o)) return true 49 | return false 50 | } 51 | 52 | /** 53 | * Naive copy-on-write set by a frozen js Map. Placeholder since it doesn't 54 | * properly honour Piglet value semantics on keys, but works ok with keywords 55 | * since we intern those. 56 | */ 57 | export default class HashSet { 58 | constructor(metadata, m) { 59 | assert(m instanceof Map, "HashSet entries has to be js:Map") 60 | const self = function(value, fallback) { 61 | if (self.has(value)) return value 62 | return fallback === undefined ? null : fallback 63 | } 64 | Object.setPrototypeOf(self, this.constructor.prototype) 65 | fixed_prop(self, "entries", Object.freeze(m || new Map())) 66 | if (metadata) { 67 | set_meta(self, metadata) 68 | } 69 | self._count = Array.from(self.entries.values()).reduce((acc,bucket)=>acc+bucket.length, 0) 70 | return self 71 | } 72 | 73 | static of(meta, ...values) { 74 | const m = new Map() 75 | for (const o of values) madd(m, o) 76 | return new this(meta, m) 77 | } 78 | 79 | conj(o) { 80 | if (this.has(o)) return this 81 | return new HashSet(meta(this), madd(new Map(this.entries), o)) 82 | } 83 | 84 | disj(o) { 85 | if (this.has(o)) return new HashSet(meta(this), mremove(new Map(this.entries), o)) 86 | return this 87 | } 88 | 89 | with_meta(m) { 90 | return new HashSet(m, this.entries) 91 | } 92 | 93 | [Symbol.iterator]() { 94 | const map_it = this.entries[Symbol.iterator]() 95 | let map_res = map_it.next() 96 | let bucket_it = null 97 | let bucket_res = null 98 | const next = function next() { 99 | // Get a new bucket_it, or decide that we're done 100 | if (!bucket_it || bucket_res.done) { 101 | if (map_res.done) { 102 | return {done: true} 103 | } 104 | bucket_it = map_res.value[1][Symbol.iterator]() 105 | map_res = map_it.next() 106 | } 107 | // at this point we must have a bucket iterator 108 | bucket_res = bucket_it.next() 109 | // We're at the end of the bucket, retry with the next 110 | if (bucket_res.done) return next() 111 | return bucket_res 112 | } 113 | return {next: next} 114 | } 115 | 116 | has(o) { 117 | return mhas(this.entries, o) 118 | } 119 | 120 | count() { 121 | return this._count 122 | } 123 | 124 | emit(cg) { 125 | return cg.invoke_var(this, 126 | "piglet", "lang", "set-ctor", 127 | [ 128 | cg.emit(this, meta(this) || null), 129 | ...Array.from(this).map((o)=>cg.emit(this, o)) 130 | ]) 131 | } 132 | 133 | inspect() { 134 | let recur = (v)=> (typeof v === 'object' || typeof v === 'function') && v?.inspect ? v.inspect() : Repr.satisfied(v) ? Repr._repr(v) : v 135 | return `HashSet(${Array.from(this, ((o)=>`${recur(o)}`)).join(", ")})` 136 | } 137 | 138 | toJSON() { 139 | return Array.from(this.entries.values).reduce((acc, bucket)=>acc+bucket, []) 140 | } 141 | } 142 | 143 | export const EMPTY = new HashSet(null, new Map()) 144 | 145 | HashSet.EMPTY = EMPTY 146 | 147 | export function set(coll) { return HashSet.of(meta(coll), ...(coll || [])) } 148 | -------------------------------------------------------------------------------- /lib/piglet/lang/IteratorSeq.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import AbstractSeq from "./AbstractSeq.mjs" 4 | 5 | export default class IteratorSeq extends AbstractSeq { 6 | constructor(iterator, value, done) { 7 | super() 8 | this.value = value 9 | this.done = done 10 | this._rest = null 11 | this.iterator = iterator 12 | } 13 | 14 | static of(iterator) { 15 | const {value, done} = iterator.next() 16 | if (done) return null 17 | return new this(iterator, value, done) 18 | } 19 | 20 | static of_iterable(iterable) { 21 | let self = this.of(iterable[Symbol.iterator]()) 22 | self && (self.iterable = iterable) 23 | return self 24 | } 25 | 26 | first() { 27 | return this.value 28 | } 29 | 30 | rest() { 31 | if (this._rest) { 32 | return this._rest 33 | } 34 | const {value, done} = this.iterator.next() 35 | if (done) { 36 | return null 37 | } 38 | this._rest = new this.constructor(this.iterator, value, done) 39 | return this._rest 40 | } 41 | 42 | seq() { 43 | return this.done ? null : this 44 | } 45 | 46 | [Symbol.iterator]() { 47 | if (this.iterable) return this.iterable[Symbol.iterator]() 48 | 49 | let head = this.seq() 50 | return {next: ()=>{ 51 | if(!head) { 52 | return {value: null, done: true} 53 | } 54 | const ret = {value: head.value, 55 | done: head.done} 56 | head = head.rest() 57 | return ret 58 | }} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/piglet/lang/Keyword.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {meta, set_meta} from "./metadata.mjs" 4 | import AbstractIdentifier from "./AbstractIdentifier.mjs" 5 | import {assert, assert_type} from "./util.mjs" 6 | 7 | export default class Keyword extends AbstractIdentifier { 8 | constructor(meta, name) { 9 | assert_type(name, String, `Expected String name, got ${name.description}`) 10 | super(meta, ":", name, name) 11 | } 12 | 13 | with_meta(m) { 14 | return new this.constructor(m, this.name) 15 | } 16 | 17 | eq(other) { 18 | return (other instanceof Keyword) && this.name === other.name 19 | } 20 | 21 | emit(cg) { 22 | return cg.invoke_var( 23 | this, 24 | "piglet", "lang", 25 | "keyword", 26 | [cg.emit(this, this.name) 27 | // , cg.emit(this, meta(this)) 28 | ] 29 | ) 30 | } 31 | } 32 | 33 | const kw_cache = new Object(null) 34 | 35 | export function keyword(name, meta) { 36 | if (!meta) 37 | return kw_cache[name]||=new Keyword(meta || null, name) 38 | return new Keyword(meta || null, name) 39 | } 40 | -------------------------------------------------------------------------------- /lib/piglet/lang/LazySeq.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Seq from "./protocols/Seq.mjs" 4 | import Seqable from "./protocols/Seqable.mjs" 5 | import Empty from "./protocols/Empty.mjs" 6 | import AbstractSeq from "./AbstractSeq.mjs" 7 | import {assert} from "./util.mjs" 8 | 9 | export default class LazySeq extends AbstractSeq { 10 | constructor(thunk) { 11 | super() 12 | this.thunk = thunk 13 | this._seq = null 14 | this.realized = false 15 | } 16 | 17 | realize() { 18 | if (!this.realized) { 19 | this._seq = this.thunk() 20 | if (Empty.satisfied(this._seq) && Empty._empty_$QMARK$_(this._seq)) { 21 | this._seq == null 22 | } 23 | this.realized = true 24 | } 25 | } 26 | 27 | first() { 28 | this.realize() 29 | return Seq._first(this._seq) 30 | } 31 | 32 | rest() { 33 | this.realize() 34 | return Seq._rest(this._seq) 35 | } 36 | 37 | seq() { 38 | this.realize() 39 | return Seqable._seq(this._seq) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/piglet/lang/List.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Sym from "./Sym.mjs" 4 | import AbstractSeq from "./AbstractSeq.mjs" 5 | import {meta, set_meta} from "./metadata.mjs" 6 | import Repr from "./protocols/Repr.mjs" 7 | 8 | export default class List extends AbstractSeq { 9 | constructor(elements) { 10 | super() 11 | this.elements = elements 12 | } 13 | 14 | first() { 15 | return this.elements[0] 16 | } 17 | 18 | rest() { 19 | if (this.elements.length > 1) { 20 | return new List(this.elements.slice(1)) 21 | } 22 | return null 23 | } 24 | 25 | seq() { 26 | return this.empty_p() ? null : this 27 | } 28 | 29 | empty_p() { 30 | return this.elements.length == 0 31 | } 32 | 33 | count() { 34 | return this.elements.length 35 | } 36 | 37 | conj(el) { 38 | const elements = [...this] 39 | elements.unshift(el) 40 | return new this.constructor(elements) 41 | } 42 | 43 | [Symbol.iterator]() { 44 | return this.elements[Symbol.iterator]() 45 | } 46 | 47 | with_meta(m) { 48 | return set_meta(new List(Array.from(this.elements)), m) 49 | } 50 | 51 | emit(cg) { 52 | return cg.invoke_var( 53 | this, 54 | "piglet", 55 | "lang", 56 | "list", 57 | Array.from(this, (e)=>cg.emit(this, e))) 58 | } 59 | 60 | inspect() { 61 | let recur = (v)=> (typeof v === 'object' || typeof v === 'function') && v?.inspect ? v.inspect() : Repr.satisfied(v) ? Repr._repr(v) : v 62 | return `List(${Array.from(this, recur).join(", ")})` 63 | } 64 | 65 | [Symbol.for('nodejs.util.inspect.custom')](depth, options, inspect) { 66 | return `${options.stylize(this.constructor.name, 'special')}(${Array.from(this, inspect).join(",")})` 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/piglet/lang/Module.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Var from "./Var.mjs" 4 | import PrefixName from "./PrefixName.mjs" 5 | import {keyword} from "./Keyword.mjs" 6 | import Keyword from "./Keyword.mjs" 7 | import Sym, {symbol, gensym} from "./Sym.mjs" 8 | import QSym from "./QSym.mjs" 9 | import {partition_n, fixed_prop, assert, assert_type} from "./util.mjs" 10 | import Context from "./Context.mjs" 11 | import {reset_meta} from "./metadata.mjs" 12 | import {munge, unmunge} from "./util.mjs" 13 | import {dict} from "./Dict.mjs" 14 | import {set_meta_computed} from "./metadata.mjs" 15 | 16 | export default class Module { 17 | constructor(opts) { 18 | this.required = false 19 | this.set_opts(opts) 20 | } 21 | 22 | set_opts(opts) { 23 | const name = opts?.get("name") 24 | const pkg = opts.get("package") 25 | assert(pkg, opts) 26 | fixed_prop(this, "package", pkg) 27 | fixed_prop(this, "pkg", pkg.name) 28 | fixed_prop(this, "name", name) 29 | fixed_prop(this, "munged_id", munge(name)) 30 | fixed_prop(this, "fqn", QSym.parse(`${pkg.name}:${name}`)) 31 | fixed_prop(this, "imports", opts?.get("imports") || []) 32 | fixed_prop(this, "aliases", {}) 33 | fixed_prop(this, "vars", {}) 34 | 35 | this.self_ref = gensym(`mod-${name}`) 36 | 37 | if (opts.get('location')) { 38 | this.location = opts.get('location') 39 | } 40 | 41 | for (const {alias, from} of this.imports) { 42 | this.set_alias(alias.name, from) 43 | } 44 | this.context = (opts?.get("context") || dict()).assoc("self", this.pkg) 45 | set_meta_computed(this, ()=>dict(keyword("file"), this.location, 46 | keyword("start"), 0)) 47 | } 48 | 49 | /** 50 | * Takes the current package name (string), and a `(module ...)` form, and 51 | * returns a dict with parsed module attributes (as string keys) 52 | * - name 53 | * - imports 54 | * - context 55 | */ 56 | static parse_opts(current_pkg, form) { 57 | const [_, name, ...more] = form 58 | // console.log(`Module.parse_opts(${current_pkg}, ${name.inspect()})`) 59 | let opts = dict("name", name.name, "imports", [], "context", null) 60 | for (let [kw,...vals] of more) { 61 | if (kw.name == "import") { 62 | for (let val of vals) { 63 | if (Array.isArray(val)) { 64 | let [alias, ...pairs] = val 65 | let i = {alias: alias} 66 | for (let [k, v] of partition_n(2, pairs)) { 67 | i[k.name] = v 68 | } 69 | opts.get("imports").push(i) 70 | } else { 71 | opts.get("imports").push({ 72 | alias: symbol(val.name), 73 | from: val 74 | }) 75 | } 76 | } 77 | } 78 | if (kw.name == "context") { 79 | opts = opts.assoc("context", vals[0]) 80 | } 81 | } 82 | return opts 83 | } 84 | 85 | static from(pkg, form) { 86 | // console.log(`Module.from(${pkg}, ${Array.from(form)[1].inspect()})`) 87 | return new this(this.parse_opts(pkg, form)) 88 | } 89 | 90 | merge_opts(opts) { 91 | Object.assign(this.imports, opts?.get("imports")) 92 | for (const {alias, from} of this.imports) { 93 | this.set_alias(alias.name, from) 94 | } 95 | this.context = (opts?.get("context") || dict()).assoc("self", `${this.pkg}#`) 96 | } 97 | 98 | resolve(name) { 99 | // console.log("Resolving", this.repr(), name, !! this.vars[munge(name)]) 100 | return this.vars[munge(name)] 101 | } 102 | 103 | ensure_var(name, meta) { 104 | const munged = munge(name) 105 | if (!Object.hasOwn(this.vars, name)) { 106 | this.vars[munged] = new Var(this.pkg, this.name, name, null, null, meta) 107 | } 108 | return this.vars[munged] 109 | } 110 | 111 | intern(name, value, meta) { 112 | const the_var = this.ensure_var(name, meta) 113 | the_var.set_value(value) 114 | if (meta !== undefined) { 115 | if (meta?.constructor == Object) { 116 | meta = Object.entries(meta).reduce((acc,[k,v])=>acc.assoc(k, v), dict()) 117 | } 118 | reset_meta(the_var, meta) 119 | } 120 | return the_var 121 | } 122 | 123 | has_var(name) { 124 | return !!this.vars[munge(name)] 125 | } 126 | 127 | set_alias(alias, mod) { 128 | assert_type(mod, Module, `Alias should point to Module, got ${mod?.constructor || typeof mod}`) 129 | this.aliases[alias] = mod 130 | if (mod.is_js_import && mod.resolve("default")) { 131 | this.intern(alias, mod.resolve("default").value) 132 | } else if (!this.has_var(alias)) { 133 | this.intern(alias, mod) 134 | } 135 | } 136 | 137 | resolve_alias(alias) { 138 | return this.aliases[alias] 139 | } 140 | 141 | repr() { 142 | return `${this.pkg}:${this.name}` 143 | } 144 | 145 | static munge(id) {return munge(id)} 146 | static unmunge(id) {return unmunge(id)} 147 | 148 | inspect() { 149 | return `Module(${this.pkg}:${this.name})` 150 | } 151 | 152 | // DictLike 153 | keys() { return Array.from(Object.keys(this.vars), (k)=>new Sym(null, null, unmunge(k), null))} 154 | values() { return Object.values(this.vars)} 155 | lookup(k) { return this.vars[munge(k)] } 156 | seq() { 157 | if (Object.keys(this.vars).length === 0) return null 158 | return Array.from(Object.entries(this.vars), ([k, v])=>[new Sym(null, null, unmunge(k), null), v]) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/piglet/lang/ModuleRegistry.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Module from "./Module.mjs" 4 | import Package from "./Package.mjs" 5 | import PrefixName from "./PrefixName.mjs" 6 | import Sym from "./Sym.mjs" 7 | import QSym from "./QSym.mjs" 8 | import {PIGLET_PKG, assert, assert_type, munge, fixed_prop, fixed_props} from "./util.mjs" 9 | import {dict} from "./Dict.mjs" 10 | import QName from "./QName.mjs" 11 | 12 | function qname(s) { return QName.parse(s) } 13 | 14 | const pkg$name = qname('https://vocab.piglet-lang.org/package/name') 15 | const pkg$deps = qname('https://vocab.piglet-lang.org/package/deps') 16 | const pkg$location = qname('https://vocab.piglet-lang.org/package/location') 17 | const pkg$paths = qname('https://vocab.piglet-lang.org/package/paths') 18 | 19 | export default class ModuleRegistry { 20 | constructor() { 21 | fixed_props(this, {packages: {}, index: {}}) 22 | const piglet_lang = this.ensure_module(PIGLET_PKG, "lang") 23 | this.current_module = piglet_lang.ensure_var("*current-module*") 24 | } 25 | 26 | static instance() { 27 | return this._instance ||= new this() 28 | } 29 | 30 | find_package(name) { 31 | assert(name.includes('://') && new URL(name).pathname.indexOf(":") === -1, 32 | `Package name must be single segment URI, got ${name}`) 33 | return this.packages[name] 34 | } 35 | 36 | ensure_package(name) { 37 | assert(name.includes('://') && new URL(name).pathname.indexOf(":") === -1, 38 | `Package name must be single segment URI, got ${name}`) 39 | if (!(name in this.packages)) { 40 | const pkg = new Package(name) 41 | this.packages[name] = pkg 42 | fixed_prop(this.index, 43 | name, 44 | // name === PIGLET_PKG ? 'piglet' : name, 45 | pkg.index) 46 | } 47 | return this.packages[name] 48 | } 49 | 50 | package_from_spec(pkg_spec) { 51 | const pkg_name = pkg_spec.get(pkg$name) 52 | const pkg = this.ensure_package(pkg_name.toString()) 53 | pkg.paths = pkg_spec.get(pkg$paths) || [] 54 | pkg.deps = pkg_spec.get(pkg$deps) || dict() 55 | pkg.location = pkg_spec.get(pkg$location).toString() 56 | return pkg 57 | } 58 | 59 | find_module(pkg, mod) { 60 | [pkg, mod] = this.split_mod_name(pkg, mod) 61 | return this.packages[pkg]?.find_module(mod) 62 | } 63 | 64 | ensure_module(pkg, mod) { 65 | [pkg, mod] = this.split_mod_name(pkg, mod) 66 | const module = this.ensure_package(pkg).ensure_module(mod) 67 | if (!(pkg == PIGLET_PKG && mod == "lang")) { 68 | Object.setPrototypeOf(module.vars, this.find_module(PIGLET_PKG, "lang").vars) 69 | } 70 | return module 71 | } 72 | 73 | resolve_module(current_package_name, from) { 74 | const current_package = this.ensure_package(current_package_name) 75 | if (typeof from === 'string') { 76 | return this.ensure_module(current_package_name, `js-interop/${from.replace(':', '__').replaceAll('../','')}`) 77 | } else if (from instanceof Sym) { 78 | if (from.mod) { 79 | return this.ensure_module(current_package.aliases[from.mod], from.name) 80 | } else { 81 | return this.ensure_module(current_package_name, from.name) 82 | } 83 | } else if (from instanceof QSym) { 84 | return this.ensure_module(from) 85 | } else { 86 | throw new Error(`Bad type for :from ${from} ${from?.constructor?.name || typeof from}`) 87 | } 88 | } 89 | 90 | register_module({pkg, name, imports, context, location, self_ref}) { 91 | const mod = this.ensure_module(pkg, name) 92 | while(mod.imports.shift()) {} 93 | Object.keys(mod.aliases).forEach((k)=>delete mod.aliases[k]) 94 | for (const {alias, from} of imports) { 95 | const import_mod = this.resolve_module(pkg, from) 96 | mod.imports.push({alias: alias, from: import_mod}) 97 | mod.aliases[alias] = import_mod 98 | } 99 | mod.location = location 100 | mod.context = context 101 | mod.self_ref = self_ref 102 | return mod 103 | } 104 | 105 | /** 106 | * Takes the current Package and a `(module ...)` form, and returns a Module 107 | * object. The module is added to the registry as part of the 108 | * current_package, and any imports aliases are added (but not loaded!). 109 | */ 110 | parse_module_form(current_package, module_form) { 111 | const mod_opts = Module.parse_opts(current_package.name, module_form) 112 | const module = this.ensure_module(current_package.name, mod_opts.get('name')) 113 | module.merge_opts(mod_opts.assoc('imports', mod_opts.get('imports').map(({alias, from})=>{ 114 | if (typeof from === 'string') { 115 | return {js_module: true, 116 | module_path: from, 117 | alias: alias, 118 | from: this.resolve_module(current_package.name, from)} 119 | } 120 | return {alias: alias, from: this.resolve_module(current_package.name, from)} 121 | }))) 122 | return module 123 | } 124 | 125 | /** 126 | * Helper method to extract the package+mod names, either from a given 127 | * PrefixName (1-arg), from a single String arg (gets split by colon 128 | * separator), or from explicitly given String args for pkg and mod (2-args) 129 | */ 130 | split_mod_name(pkg, mod) { 131 | if (!mod) { 132 | if (typeof pkg === 'string' && pkg.includes('://')) { 133 | pkg = QSym.parse(pkg) 134 | } 135 | if (pkg instanceof PrefixName) { 136 | throw "PrefixName used where package name (Sym or QSym) was expected" 137 | } else if (pkg instanceof Sym) { 138 | // When identifying a package with a symbol the two components 139 | // (pkg/mod) are assigned to the mod/var fields. This is an 140 | // implementation detail that really only occurs in module forms. 141 | mod = pkg.name 142 | pkg = pkg.mod 143 | } else if (pkg instanceof QSym) { 144 | mod = pkg.mod 145 | pkg = pkg.pkg 146 | } else if (pkg instanceof String) { 147 | const parts = pkg.split(":") 148 | switch (parts.length) { 149 | case 1: 150 | mod = parts[0] 151 | pkg = this.current_module.deref().pkg 152 | break 153 | case 2: 154 | pkg = parts[0] || this.current_module.deref().pkg 155 | mod = parts[1] 156 | break 157 | } 158 | } 159 | 160 | } 161 | assert_type(pkg, 'string') 162 | assert_type(mod, 'string') 163 | return [pkg, mod] 164 | } 165 | 166 | inspect() { 167 | let s = "\nModuleRegistry(\n" 168 | for (const [pkg_name, pkg] of Object.entries(this.packages)) { 169 | for (const [mod_name, mod] of Object.entries(pkg.modules)) { 170 | s += `${pkg_name}:${mod_name}=${mod.inspect()},\n` 171 | } 172 | } 173 | s+=")" 174 | return s 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/piglet/lang/Package.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {PIGLET_PKG, assert, munge, fixed_props, fixed_prop} from "./util.mjs" 4 | import Module from "./Module.mjs" 5 | import {dict} from "./Dict.mjs" 6 | 7 | export default class Package { 8 | constructor(name) { 9 | fixed_props( 10 | this, 11 | {name: name, 12 | modules: {}, 13 | aliases: {}, 14 | index: {}}) 15 | fixed_prop(this.aliases, 'piglet', PIGLET_PKG) 16 | } 17 | 18 | find_module(mod) { 19 | if (typeof mod === 'string') { 20 | const munged_mod = munge(mod) 21 | if (munged_mod in this.modules) { 22 | return this.modules[munged_mod] 23 | } 24 | return null 25 | } 26 | throw `Unexpected type ${mod?.constructor || typeof mod}` 27 | } 28 | 29 | ensure_module(mod) { 30 | const munged = munge(mod) 31 | if (!(munged in this.modules)) { 32 | const module = new Module(dict("package", this, "pkg", this.name, "name", mod)) 33 | this.modules[munged] = module 34 | this.index[munged] = module.vars 35 | } 36 | return this.modules[munged] 37 | } 38 | 39 | add_alias(from, to) { 40 | assert(!(from in this.aliases), `Alias ${from} already exists: ${this.aliases[from]}`) 41 | this.aliases[from] = to 42 | } 43 | 44 | resolve_alias(alias) { 45 | const resolved = this.aliases[alias] 46 | assert(resolved, `Alias ${alias} not found in ${this.name} ${this.aliases}`) 47 | return resolved 48 | } 49 | 50 | inspect() { 51 | return `Package(${this.name})` 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/piglet/lang/PrefixName.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {assert} from "./util.mjs" 4 | import AbstractIdentifier from "./AbstractIdentifier.mjs" 5 | import Lookup from "./protocols/Lookup.mjs" 6 | 7 | export default class PrefixName extends AbstractIdentifier { 8 | constructor(meta, prefix, suffix) { 9 | assert(prefix === null || !prefix.includes(":"), "prefix can not contain a colon") 10 | assert(!suffix.includes(":"), "suffix can not contain a colon") 11 | super(meta, ":", suffix, `${prefix || ""}:${suffix}`) 12 | this.prefix = prefix 13 | this.suffix = suffix 14 | } 15 | 16 | static parse(s) { 17 | assert(!s.includes("://"), "PrefixName can not contain '://'") 18 | const parts = s.split(":") 19 | assert(parts.length == 2, "PrefixName can only contain one colon") 20 | const [pre, suf] = parts 21 | return new this(null, pre, suf) 22 | } 23 | 24 | emit(cg) { 25 | return cg.invoke_var( 26 | this, 27 | "piglet", 28 | "lang", 29 | "prefix-name", 30 | [this.prefix, this.suffix].map(s=>cg.literal(this, s))) 31 | } 32 | 33 | with_prefix(prefix) { 34 | return Object.assign(new this.constructor(this.prefix, this.suffix), this, {prefix: prefix}) 35 | } 36 | 37 | with_meta(m) { 38 | return new this.constructor(m, this.prefix, this.suffix) 39 | } 40 | 41 | call(_, arg, fallback) { 42 | if (fallback === undefined) { 43 | return Lookup._get(arg, this) 44 | } 45 | return Lookup._get(arg, this, fallback) 46 | } 47 | 48 | inspect() { 49 | return `PrefixName(:${this.prefix}:${this.suffix})` 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/piglet/lang/Protocol.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {partition_n, munge, unmunge} from "./util.mjs" 4 | import {meta, set_meta} from "./metadata.mjs" 5 | 6 | const symreg = {} 7 | function symbol_for(str) { 8 | return symreg[str] ||= Symbol(str) 9 | } 10 | 11 | function meta_arities(o) { 12 | const m = meta(o) 13 | if (m) return m.get_kw("arities") 14 | } 15 | 16 | const null_proto = {} 17 | 18 | function find_proto(o) { 19 | if (o === null || o === undefined) return null_proto 20 | if (Array.isArray()) return Array.prototype 21 | 22 | switch (typeof o) { 23 | case "object": 24 | return Object.getPrototypeOf(o) || Object.prototype 25 | case "boolean": 26 | return Boolean.prototype 27 | case "number": 28 | return Number.prototype 29 | case "bigint": 30 | return BigInt.prototype 31 | case "symbol": 32 | return Symbol.prototype 33 | case "function": 34 | return Function.prototype 35 | case "string": 36 | return String.prototype 37 | } 38 | } 39 | 40 | function stringify_object(o) { 41 | if (null == o) { 42 | return `${o}` 43 | } 44 | if ("function" === typeof o.toJSON) { 45 | return `${o.toJSON()}` 46 | } 47 | if (`${o}` !== "[object Object]") { 48 | return `${o}` 49 | } 50 | if (Object.entries(o).length > 0) { 51 | return `{${ 52 | Object.entries(o).map(([k,v])=>`${k}: ${v}`).join(", ") 53 | }}` 54 | } 55 | return o.toString() 56 | } 57 | 58 | export default class Protocol { 59 | constructor(meta, module_name, proto_name, signatures) { 60 | this.fullname = `${module_name}:${proto_name}` 61 | this.sentinel = symbol_for(this.fullname) 62 | 63 | set_meta(this, meta) 64 | this.module_name = module_name 65 | this.name = proto_name 66 | this.methods = {} 67 | 68 | for(let signature of signatures) { 69 | const [method_name, arities] = signature 70 | const method_fullname = `${module_name}:${proto_name}:${method_name}` 71 | const arity_map = arities.reduce( 72 | (acc, [argv, doc])=>{ 73 | acc[argv.length] = { 74 | name: method_name, 75 | argv: argv, 76 | arity: argv.length, 77 | doc: doc, 78 | sentinel: symbol_for(`${method_fullname}/${argv.length}`)} 79 | return acc}, 80 | {}) 81 | 82 | this.methods[method_name] = { 83 | name: method_name, 84 | fullname: method_fullname, 85 | arities: arity_map 86 | } 87 | this[munge(method_name)]=function(obj) { 88 | let fn 89 | if (fn = obj?.[arity_map[arguments.length]?.sentinel]) { 90 | return fn.apply(null, arguments) 91 | } 92 | if (arguments.length === 0) { 93 | throw new Error("Protocol method called without receiver.") 94 | } 95 | return this.invoke(arity_map, ...arguments) 96 | } 97 | } 98 | } 99 | 100 | intern(mod) { 101 | mod.intern(this.name, this) 102 | for (let {name, fullname, arities} of Object.values(this.methods)) { 103 | mod.intern(name, (obj, ...args)=>{ 104 | const method = arities[args.length+1] 105 | if (!method) { 106 | throw new Error(`Wrong number of arguments to protocol ${fullname}, got ${args.length}, expected ${Object.keys(arities)}`) 107 | } 108 | const fn = (obj && obj[method.sentinel]) || find_proto(obj)[method.sentinel] 109 | if (!fn) { 110 | throw new Error(`No protocol method ${fullname} found in ${obj?.constructor?.name} ${stringify_object(obj)}`) 111 | } 112 | return fn(obj, ...args) 113 | }, null 114 | // We use Dict instances for metadata, but we can't 115 | // include Dict here... This metadata doesn't seem to be 116 | // load bearing at the moment, although it would be nice 117 | // to have. 118 | // {"protocol-method?": true, sentinel: fullname} 119 | ) 120 | } 121 | return this 122 | } 123 | 124 | satisfied(o) { 125 | if (o === null || o === undefined) { 126 | return !!null_proto[this.sentinel] 127 | } 128 | return !!o[this.sentinel] 129 | } 130 | 131 | method_sentinel(method_name, arity) { 132 | if (!this.methods[method_name]) { 133 | throw new Error(`No method ${method_name} in ${this.fullname}, expected ${Object.getOwnPropertyNames(this.methods)}`) 134 | } 135 | if (!this.methods[method_name].arities[arity]) { 136 | throw new Error(`Unknown arity for ${method_name} in ${this.fullname}, got ${arity}, expected ${Object.getOwnPropertyNames(this.methods[method_name].arities)}`) 137 | } 138 | return this.methods[method_name].arities[arity].sentinel 139 | } 140 | 141 | extend_object2(object, function_map) { 142 | object[this.sentinel] = true 143 | for (let [name_arity, fn] of Object.entries(function_map)) { 144 | const [method_name, arity_string] = name_arity.split("/") 145 | const arity = parseInt(arity_string, 10) 146 | object[this.method_sentinel(method_name, arity)] = fn 147 | } 148 | } 149 | 150 | extend2(...args) { 151 | for (let [klass, functions] of partition_n(2, args)) { 152 | const proto = klass === null ? null_proto : klass.prototype 153 | this.extend_object2(proto, functions) 154 | } 155 | return this 156 | } 157 | 158 | invoke(arities, obj) { 159 | let fn 160 | const arg_count = arguments.length - 1 161 | const method = arities[arg_count] 162 | if (!method) { 163 | throw new Error(`Wrong number of arguments to protocol method ${this.fullname}, got ${arg_count - 1}, expected ${Object.keys(arities)}`) 164 | } 165 | fn = (obj && obj[method.sentinel]) 166 | if (!fn) { 167 | const proto = find_proto(obj) // for null and primitives 168 | if(!proto) { 169 | throw new Error(`${method.sentinel.description}: Failed to resolve prototype on ${obj.toString ? obj : JSON.stringify(obj)} ${typeof obj}`) 170 | } 171 | fn = proto[method.sentinel] 172 | } 173 | if (!fn) { 174 | throw new Error(`No protocol method ${method.name} found in ${obj?.constructor?.name} ${stringify_object(obj)}`) 175 | } 176 | return fn(obj, ...Array.prototype.slice.call(arguments, 2)) 177 | } 178 | } 179 | 180 | export function extend_class(klass, ...args) { 181 | const proto = klass === null ? null_proto : klass.prototype 182 | for (let [fullname, functions] of partition_n(2, args)) { 183 | proto[symbol_for(fullname)] = true 184 | for (var fn of functions) { 185 | let name = unmunge(fn.name) 186 | let arity = fn.length 187 | proto[symbol_for(`${fullname}:${name}/${arity}`)] = fn 188 | } 189 | } 190 | return klass 191 | } 192 | -------------------------------------------------------------------------------- /lib/piglet/lang/QName.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {assert} from "./util.mjs" 4 | import AbstractIdentifier from "./AbstractIdentifier.mjs" 5 | import Lookup from "./protocols/Lookup.mjs" 6 | import Named from "./protocols/Named.mjs" 7 | import {fixed_prop} from "./util.mjs" 8 | 9 | /** 10 | * Fully qualified identifier 11 | * 12 | * In behavior this acts exactly as a Keyword, but instead of representing a 13 | * simple name, it represents a fully qualified identifier in the form of a 14 | * URI/IRI. 15 | * 16 | * QNames function in conjunction with the `*current-context*`. Any PrefixName 17 | * in source code will be expanded during compilation into a QName. Conversely 18 | * the printer will print out QNames as PrefixNames. In either case the context 19 | * is consulted to find which prefixes map to which base URLs. 20 | * 21 | * QNames are mainly modeled on RDF identifiers, but we also use QNames to 22 | * represent XML namespaced tags. These however are two different conceptual 23 | * models. In RDF a prefix+suffix are combined to form a full IRI identifier. In 24 | * XML the namespace and tagName are not usually combined as such, but instead 25 | * function more like a pair. This means that for XML applications (and other 26 | * places where these kind of semantics are needed) we need to track what the 27 | * prefix and what the suffix is, and possibly add a separator between the two, 28 | * because XML namespaces will often not end on a `/` or `#` but simply on an 29 | * alphabetic character. 30 | * 31 | * Hence why we have multiple overlapping properties here. 32 | * - fqn: the fully qualified name as a string, most applications will only need this 33 | * - base / separator / suffix: the FQN split into three parts, mainly for XML 34 | * 35 | * Whether the latter ones are all set will depend on the construction. They 36 | * will be if the QName originates from a PrefixName, and the use of a separator 37 | * can be configured via the context. 38 | */ 39 | export default class QName extends AbstractIdentifier { 40 | constructor(meta, base, separator, suffix) { 41 | const fqn = `${base}${separator || ""}${suffix || ""}` 42 | assert(fqn.includes("://"), "QName must contain '://'") 43 | super(meta, ":", fqn, fqn) 44 | fixed_prop(this, "fqn", fqn) 45 | fixed_prop(this, "base", base) 46 | fixed_prop(this, "separator", separator || '') 47 | fixed_prop(this, "suffix", suffix || '') 48 | } 49 | 50 | with_meta(m) { 51 | return new this.constructor(m, this.fqn) 52 | } 53 | 54 | emit(cg) { 55 | return cg.invoke_var( 56 | this, 57 | "piglet", "lang", "qname", 58 | [cg.literal(this, this.base), 59 | cg.literal(this, this.separator), 60 | cg.literal(this, this.suffix)]) 61 | } 62 | 63 | static parse(s) { 64 | return new this(null, s) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/piglet/lang/QSym.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {assert, fixed_prop} from "./util.mjs" 4 | import AbstractIdentifier from "./AbstractIdentifier.mjs" 5 | import Lookup from "./protocols/Lookup.mjs" 6 | import Named from "./protocols/Named.mjs" 7 | 8 | export default class QSym extends AbstractIdentifier { 9 | constructor(meta, fqn) { 10 | assert(fqn?.includes && fqn.includes("://"), `QSym must contain '://', got ${fqn}`) 11 | const url = new URL(fqn) 12 | const [pkg, mod] = decodeURI(url.pathname).split(":") 13 | const name = fqn.split(":").slice(-1)[0] 14 | super(meta, "", name, fqn) 15 | fixed_prop(this, "fqn", fqn) 16 | fixed_prop(this, "pkg", url.origin === "null" ? `${url.protocol}//${pkg}` : `${url.origin}${pkg}`) 17 | fixed_prop(this, "mod", mod) 18 | } 19 | 20 | with_meta(m) { 21 | return new this.constructor(m, this.fqn) 22 | } 23 | 24 | with_mod(mod_name) { 25 | this.constructor.parse(`${this.pkg}:${mod_name}`) 26 | } 27 | 28 | emit(cg) { 29 | return cg.invoke_var( 30 | this, 31 | "piglet", "lang", "qsym", 32 | [cg.literal(this, this.fqn)]) 33 | } 34 | 35 | static parse(s) { 36 | return new this(null, s) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/piglet/lang/Range.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {set_meta} from "./metadata.mjs" 4 | import AbstractSeq from "./AbstractSeq.mjs" 5 | import SeqIterator from "./SeqIterator.mjs" 6 | 7 | export default class Range extends AbstractSeq { 8 | constructor(from, to, step, meta) { 9 | super() 10 | this.from = from 11 | this.to = to 12 | this.step = step 13 | set_meta(this, meta) 14 | } 15 | 16 | static range0() { 17 | return new Range(0, undefined, 1) 18 | } 19 | 20 | static range1(to) { 21 | return new Range(0, to, 1) 22 | } 23 | 24 | static range2(from, to) { 25 | return new Range(from, to, 1) 26 | } 27 | 28 | static range3(from, to, step) { 29 | return new Range(from, to, step) 30 | } 31 | 32 | [Symbol.iterator]() { 33 | if (this.to === undefined) { 34 | throw new Error("Can't get range iterator for infinte range") 35 | } 36 | let i = this.from 37 | return {next: ()=>{ 38 | const v = i 39 | if (v < this.to) { 40 | i+=this.step 41 | return {value: v} 42 | } 43 | return {done: true} 44 | }} 45 | } 46 | 47 | first() { 48 | if (this.to === undefined || (this.from < this.to)) { 49 | return this.from 50 | } 51 | return null 52 | } 53 | 54 | rest() { 55 | if (this.to === undefined || ((this.from + this.step) < this.to)) { 56 | return new Range(this.from+this.step, this.to, this.step) 57 | } 58 | return null 59 | } 60 | 61 | seq() { 62 | return this.empty_p() ? null : this 63 | } 64 | 65 | count() { 66 | if (!this.to) { 67 | throw new Error("Can't count infinte range") 68 | } 69 | return Math.max(0, Math.floor((this.to - this.from) / this.step)) 70 | } 71 | 72 | empty_p() { 73 | return this.from >= this.to 74 | } 75 | 76 | inspect() { 77 | return `Range(${this.from}, ${this.to}, ${this.step})` 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/piglet/lang/Repeat.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import AbstractSeq from "./AbstractSeq.mjs" 4 | 5 | export default class Repeat extends AbstractSeq { 6 | constructor(count, value) { 7 | super() 8 | this.count = count 9 | this.value = value 10 | } 11 | 12 | first() { 13 | if (this.count === null || this.count > 0) { 14 | return this.value 15 | } 16 | return null 17 | } 18 | 19 | rest() { 20 | if (this._rest === undefined) { 21 | if (this.count === null) { 22 | this._rest = this 23 | } else if (this.count <= 1) { 24 | this._rest = null 25 | } else { 26 | this._rest = new this.constructor(this.count-1, this.value) 27 | } 28 | } 29 | return this._rest 30 | } 31 | 32 | seq() { 33 | return this.count <= 0 ? null : this 34 | } 35 | 36 | count() { 37 | if (this.count) { 38 | return this.count 39 | } 40 | throw new Error("Can't count infinite seq") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/piglet/lang/SeqIterator.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Seq from "./protocols/Seq.mjs" 4 | 5 | export default class SeqIterator { 6 | constructor(seq) { 7 | this.seq = seq 8 | } 9 | next() { 10 | if (this.seq) { 11 | const value = Seq._first(this.seq) 12 | this.seq = Seq._rest(this.seq) 13 | return {value: value, done: false} 14 | } 15 | return {value: void(0), done: true} 16 | } 17 | [Symbol.iterator]() { 18 | return this 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/piglet/lang/Sym.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {assert, fixed_prop} from "./util.mjs" 4 | import {meta, set_meta} from "./metadata.mjs" 5 | import AbstractIdentifier from "./AbstractIdentifier.mjs" 6 | import QSym from "./QSym.mjs" 7 | 8 | const CACHE = Object.create(null) 9 | 10 | /** 11 | * Identifier with up to three components: `pkg:mod:name`. If fewer components 12 | * are given, then they anchor towards the end. So `pkg` can be nil, or `pkg` and 13 | * `mod` can both be nil. `name` must always be present. 14 | * 15 | * Generally used to refer to a var within a module within a package, hence 16 | * these names. But in some places we use syms to identify modules (e.g. in a 17 | * module form's `:import` declaration), in which case `pkg` will be `nil`, 18 | * `mod` identifies a package alias, and `name` identifies the module. 19 | */ 20 | export default class Sym extends AbstractIdentifier { 21 | constructor(pkg, mod, name, meta) { 22 | assert(name, "Sym's name can not be null") 23 | assert(!pkg || mod, "A Sym with a package must also declare a module") 24 | 25 | let id_str = name 26 | if (mod) id_str = mod + ":" + id_str 27 | if (pkg) id_str = pkg + ":" + id_str 28 | super(meta || null, "", name, id_str) 29 | 30 | fixed_prop(this, "pkg", pkg || null) 31 | fixed_prop(this, "mod", mod || null) 32 | } 33 | 34 | static parse(s, meta) { 35 | if (s.includes("://")) { 36 | return QSym.parse(s) 37 | } 38 | const [a,b,c] = s.split(":") 39 | if (meta == null) { 40 | const sym = CACHE[s] 41 | if (sym) return sym 42 | } 43 | const sym = (c ? new this(a,b,c, meta || null) : 44 | b ? new this(null,a,b, meta || null) : 45 | new this(null, null, a, meta || null)) 46 | if (meta == null) CACHE[s] = sym 47 | return sym 48 | } 49 | 50 | eq(other) { 51 | return (other instanceof Sym) && this.name === other.name && this.mod === other.mod && this.pkg === other.pkg 52 | } 53 | 54 | with_name(n) { 55 | return new this.constructor(this.pkg, this.mod, n, meta(this)) 56 | } 57 | 58 | with_mod(m) { 59 | return new this.constructor(this.pkg, m, this.name, meta(this)) 60 | } 61 | 62 | with_meta(m) { 63 | return new this.constructor(this.pkg, this.mod, this.name, m) 64 | } 65 | 66 | emit(cg) { 67 | return cg.invoke_var( 68 | this, 69 | "piglet", 70 | "lang", 71 | "symbol", 72 | [cg.literal(this, this.pkg), 73 | cg.literal(this, this.mod), 74 | cg.literal(this, this.name) 75 | //cg.emit(this, meta(this)) 76 | ]) 77 | } 78 | } 79 | 80 | export function symbol(pkg, mod, name, metadata) { 81 | if (arguments.length === 1) { 82 | return Sym.parse(pkg, metadata) 83 | } 84 | return new Sym(pkg, mod, name, metadata) 85 | } 86 | 87 | const gensym = (function() { 88 | const syms = {} 89 | return function gensym(str) { 90 | const i = (syms[str] = (syms[str] || 0) + 1) 91 | return Sym.parse(`${str}${i}`) 92 | } 93 | })() 94 | export {gensym} 95 | -------------------------------------------------------------------------------- /lib/piglet/lang/Var.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {munge, valid_property_name_p, reserved_keyword_p} from "./util.mjs" 4 | import {meta, set_meta_mutable} from "./metadata.mjs" 5 | import Repr from "./protocols/Repr.mjs" 6 | import QSym from "./QSym.mjs" 7 | 8 | let var_counter = 0 9 | function next_global_name() { 10 | let n = "", c = var_counter 11 | while (c > 25) { 12 | n += String.fromCharCode(97 + c%26) 13 | c = Math.floor(c/26) 14 | } 15 | n += String.fromCharCode(97 + c) 16 | var_counter+=1 17 | return n 18 | } 19 | 20 | const GLOBAL = typeof window === 'undefined' ? global : window 21 | 22 | function js_name(n) { 23 | if (n.endsWith("?")) n = `${n.slice(0, -1)}_p` 24 | if (reserved_keyword_p(n)) n = `${n}$` 25 | if (!valid_property_name_p(n)) n = munge(n) 26 | return n 27 | } 28 | 29 | export default class Var { 30 | constructor(pkg, module, name, value, _meta) { 31 | const self = {[name](...args) { 32 | try { 33 | return self.value(...args) 34 | } catch (e) { 35 | if (typeof e === 'object') { 36 | e.stack = ` at ${self.repr()} ${Repr._repr(meta(self))}\n${e.stack}` 37 | } 38 | throw e 39 | } 40 | }}[name] 41 | Object.setPrototypeOf(self, Var.prototype) 42 | self.fqn = new QSym(null, `${pkg}:${module}:${name}`) 43 | self.pkg = pkg 44 | self.module = module 45 | self.value = value 46 | self.binding_stack = [] 47 | self.js_name = js_name(name) 48 | // self.global_name = next_global_name() 49 | // GLOBAL[self.global_name] = self 50 | set_meta_mutable(self, _meta) 51 | return self 52 | } 53 | 54 | deref() { 55 | return this.value 56 | } 57 | 58 | set_value(value) { 59 | this.value = value 60 | return this 61 | } 62 | 63 | push_binding(value) { 64 | this.binding_stack.unshift(this.value) 65 | this.value = value 66 | } 67 | 68 | pop_binding() { 69 | this.value = this.binding_stack.shift() 70 | } 71 | 72 | repr() { 73 | return `#'${this.module}:${this.name}` 74 | } 75 | 76 | toString() { 77 | return this.repr() 78 | } 79 | 80 | inspect() { 81 | return `Var(${this.toString()})` 82 | } 83 | } 84 | 85 | Object.setPrototypeOf(Var.prototype, Function) 86 | -------------------------------------------------------------------------------- /lib/piglet/lang/hashing.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Hashable from './protocols/Hashable.mjs' 4 | 5 | import Sym from "./Sym.mjs" 6 | import Keyword, {keyword} from "./Keyword.mjs" 7 | import QName from "./QName.mjs" 8 | import PrefixName from "./PrefixName.mjs" 9 | import Context from "./Context.mjs" 10 | import AbstractIdentifier from "./AbstractIdentifier.mjs" 11 | 12 | import AbstractSeq from "./AbstractSeq.mjs" 13 | import Cons from "./Cons.mjs" 14 | import Dict from "./Dict.mjs" 15 | import IteratorSeq from "./IteratorSeq.mjs" 16 | import List from "./List.mjs" 17 | import Range from "./Range.mjs" 18 | import SeqIterator from "./SeqIterator.mjs" 19 | import LazySeq from "./LazySeq.mjs" 20 | import Repeat from "./Repeat.mjs" 21 | 22 | import {hash_bytes, hash_str, hash_combine, hash_num} from "./xxhash32.mjs" 23 | export {hash_bytes, hash_str, hash_combine, hash_num} 24 | 25 | const HASH_CACHE = new WeakMap() 26 | // Symbols don't generally work in weakmaps yet, and they are frozen so we can't 27 | // cache the hash on the symbol object... so we keep them all. Not ideal, 28 | // potential memory leak. 29 | const SYMBOL_HASH_CACHE = {} 30 | 31 | export function hash_code(o) { 32 | if (o == null) return 0 33 | const t = typeof o 34 | if ("object" === t || "function" === t) { 35 | // This doesn't seem to be generally supported yet. 36 | // https://github.com/tc39/proposal-symbols-as-weakmap-keys 37 | // || ("symbol" === t && "undefined" === typeof Symbol.keyFor(o))) { 38 | 39 | if (HASH_CACHE.has(o)) { 40 | return HASH_CACHE.get(o) 41 | } 42 | const hsh = Hashable._hash_code(o) 43 | HASH_CACHE.set(o, hsh) 44 | return hsh 45 | } 46 | 47 | if ("symbol" === t) { 48 | if (o in SYMBOL_HASH_CACHE) { 49 | return SYMBOL_HASH_CACHE[o] 50 | } 51 | return SYMBOL_HASH_CACHE[o] = Hashable._hash_code(o) 52 | } 53 | 54 | return Hashable._hash_code(o) 55 | } 56 | -------------------------------------------------------------------------------- /lib/piglet/lang/metadata.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import {assert} from "./util.mjs" 4 | 5 | export const META_SYM = Symbol("piglet:lang:meta") 6 | 7 | function assert_dict(o, v) { 8 | if (v === null) return 9 | if (v.constructor.name !== "Dict") { 10 | throw new Error(`Metadata must be a Dict, got ${v.constructor.name} ${v} ${JSON.stringify(v)} on ${o.inspect ? o.inspect() : o}`) 11 | } 12 | } 13 | 14 | export function meta(o) { 15 | const t = typeof o 16 | return (o && (t === 'object' || t === 'function') && o[META_SYM]) || null 17 | } 18 | 19 | export function has_meta(o) { 20 | return META_SYM in o 21 | } 22 | 23 | export function set_meta(o, v) { 24 | // if (meta(o)) { 25 | // console.log("ALREADY HAS META", o) 26 | // } 27 | if (v != null) { 28 | assert_dict(o, v) 29 | Object.defineProperty(o, META_SYM, {value: v, writable: false}) 30 | } 31 | return o 32 | } 33 | 34 | export function set_meta_mutable(o, v) { 35 | assert_dict(o, v) 36 | Object.defineProperty(o, META_SYM, {value: v, writable: true}) 37 | return o 38 | } 39 | 40 | export function set_meta_computed(o, f) { 41 | Object.defineProperty(o, META_SYM, {get: ()=>{const v = f(); assert_dict(o, v); return v}}) 42 | return o 43 | } 44 | 45 | export function reset_meta(o, v) { 46 | assert_dict(o, v) 47 | o[META_SYM] = v 48 | return o 49 | } 50 | -------------------------------------------------------------------------------- /lib/piglet/lang/piglet_object.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | /** 4 | * Special marker property to signal that an object/value is part of the Piglet 5 | * world. Any builtins (collections, identifiers), as well as types created 6 | * within user code (except when using the most low level constructs), get 7 | * marked as a "piglet object". 8 | * 9 | * This acts as a allowlist/denylist of sorts, in that we never treat these 10 | * objects as raw JS objects. This in turn allows us to have more convenient JS 11 | * interop, since we don't risk accidentally exposing a Piglet value as a plain 12 | * JS value. 13 | */ 14 | 15 | export const PIGOBJ_SYM = Symbol("piglet:lang:piglet-object") 16 | 17 | export function mark_as_piglet_object(o) { 18 | Object.defineProperty(o, PIGOBJ_SYM, {value: true, writable: false}) 19 | return o 20 | } 21 | 22 | export function piglet_object_p(o) { 23 | const t = typeof o 24 | return ("undefined" !== t || "null" !== t) && o[PIGOBJ_SYM] 25 | } 26 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Associative.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Associative = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Associative", 9 | [["-assoc", [[["this", "k", "v"], "Associate the given value with the given key"]]], 10 | ["-dissoc", [[["this", "k"], "Remove the association between the thegiven key and value"]]]]) 11 | 12 | export default Associative 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Conjable.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Conjable = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Conjable", 9 | [["-conj", [[["this", "e"], "Return a collection with the element added"]]]] 10 | ) 11 | export default Conjable 12 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Counted.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Counted = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Counted", 9 | [["-count", [[["this"], "The number of elements in the collection"]]]] 10 | ) 11 | 12 | export default Counted 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Derefable.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Derefable = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Derefable", 9 | [["deref", [[["this"], "Derefence a reference type"]]]] 10 | ) 11 | 12 | export default Derefable 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/DictLike.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const DictLike = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "DictLike", 9 | [["-keys", [[["this"], "Get a sequence of all keys in the dict"]]], 10 | ["-vals", [[["this"], "Get a sequence of all values in the dict"]]]] 11 | ) 12 | export default DictLike 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Empty.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Empty = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Empty", 9 | [["-empty?", [[["this"], "Is this an empty collection?"]]]] 10 | ) 11 | 12 | export default Empty 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Eq.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Eq = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Eq", 9 | [["-eq", [[["this", "that"], "Check equality"]]]] 10 | ) 11 | 12 | export default Eq 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Hashable.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Hashable = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Hashable", 9 | [["-hash-code", [[["this"], "Get a hash code for this object"]]]] 10 | ) 11 | export default Hashable 12 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Lookup.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Lookup = new Protocol( 6 | null, "piglet:lang", "Lookup", 7 | [["-get", [[["this", "k"], "Get the value associated with k, or null/undefined if absent."], 8 | [["this", "k", "default"], "Get the value associated with k, or default"]]]]) 9 | 10 | export default Lookup 11 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/MutableAssociative.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const MutableAssociative = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "MutableAssociative", 9 | [["-assoc!", [[["this", "k", "v"], "Associate the given value with the given key"]]], 10 | ["-dissoc!", [[["this", "k"], "Remove the association between the the given key and value"]]]]) 11 | 12 | export default MutableAssociative 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/MutableCollection.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol, {extend_class} from "../Protocol.mjs" 4 | 5 | const MutableCollection = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "MutableCollection", 9 | [["-conj!", [[["coll", "el"], "Add element to collection, mutates the collection, returns the collection."]]]] 10 | ) 11 | export default MutableCollection 12 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Named.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Named = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Named", 9 | [["-name", [[["this"], "Get a string representation, used for various types of identifier objects when they need to be used in a contexts where only strings are allowed"]]]] 10 | ) 11 | export default Named 12 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/QualifiedName.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | // Used for things that either "are" (QName, QSym), or "Have" (Var) a fully qualified name 6 | const QualifiedName = new Protocol( 7 | null, 8 | "piglet:lang", 9 | "QualifiedName", 10 | [["-fqn", [[["this"], "Fully qualifed name, should return an absolute URI as a string, or nil."]]]] 11 | ) 12 | 13 | export default QualifiedName 14 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Repr.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Repr = new Protocol( 6 | null, "piglet:lang", "Repr", 7 | [["-repr", [[["this"], "Return a string representation of the object"]]]]) 8 | 9 | export default Repr 10 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Seq.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Seq = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Seq", 9 | [["-first", [[["this"], "Return the first element of the seq"]]], 10 | ["-rest", [[["this"], "Return a seq of the remaining elements of this seq"]]]] 11 | ) 12 | 13 | export default Seq 14 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Seqable.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Seqable = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Seqable", 9 | [["-seq", [[["this"], "Return a seq over the collection"]]]] 10 | ) 11 | 12 | export default Seqable 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Sequential.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | import IteratorSeq from "../IteratorSeq.mjs" 5 | 6 | const Sequential = new Protocol( 7 | null, 8 | "piglet:lang", 9 | "Sequential", 10 | [] 11 | ) 12 | export default Sequential 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Swappable.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Swappable = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Swappable", 9 | [["-swap!", [[["this", "fn", "args"], "Swap the value contained in reference type by applying a function to it."]]]] 10 | ) 11 | 12 | export default Swappable 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/TaggedValue.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const TaggedValue = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "TaggedValue", 9 | [["-tag", [[["this"], ""]]], 10 | ["-tag-value", [[["this"], ""]]]] 11 | ) 12 | export default TaggedValue 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/Walkable.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const Walkable = /* @__PURE__ */ (()=>new Protocol( 6 | null, 7 | "piglet:lang", 8 | "Walkable", 9 | [["-walk", [[["this", "f"], 10 | "Apply the given function to each element in the collection, returning a collection of the same type and size"]]]] 11 | ))() 12 | 13 | export default Walkable 14 | -------------------------------------------------------------------------------- /lib/piglet/lang/protocols/WithMeta.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import Protocol from "../Protocol.mjs" 4 | 5 | const WithMeta = new Protocol( 6 | null, 7 | "piglet:lang", 8 | "WithMeta", 9 | [["-with-meta", [[["this", "meta"], "Return a version of the value with the new metadata associated with it."]]]] 10 | ) 11 | 12 | export default WithMeta 13 | -------------------------------------------------------------------------------- /lib/piglet/lang/util.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | const PIGLET_PKG = "https://piglet-lang.org/packages/piglet" 4 | export {PIGLET_PKG} 5 | 6 | export function partition_n(n, args) { 7 | const partitions = [] 8 | args = Array.from(args) 9 | for (let i = 0 ; i", "_$GT$_") 67 | .replaceAll("*", "_$STAR$_") 68 | .replaceAll("!", "_$BANG$_") 69 | .replaceAll("?", "_$QMARK$_") 70 | .replaceAll("&", "_$AMP$_") 71 | .replaceAll("%", "_$PERCENT$_") 72 | .replaceAll("=", "_$EQ$_") 73 | .replaceAll("|", "_$PIPE$_") 74 | .replaceAll("/", "_$SLASH$_") 75 | .replaceAll("@", "_$AT$_") 76 | .replaceAll(".", "$$$$") 77 | // .replaceAll("ː", "_$TRICOL$_") 78 | // .replaceAll(":", "ː") // modifier letter triangular colon U+02D0 79 | munge_cache[id]=munged 80 | unmunge_cache[munged]=id 81 | return munged 82 | } 83 | 84 | export function unmunge(id) { 85 | let unmunged = unmunge_cache[id] 86 | if (unmunged) return unmunged 87 | unmunged = id 88 | .replaceAll("$$", ".") 89 | // .replaceAll("ː", ":") 90 | // .replaceAll("_$TRICOL$_", "ː") 91 | .replaceAll("_$AT$_", "@") 92 | .replaceAll("_$SLASH$_", "/") 93 | .replaceAll("_$PIPE$_", "|") 94 | .replaceAll("_$EQ$_", "=") 95 | .replaceAll("_$PERCENT$_", "%") 96 | .replaceAll("_$AMP$_", "&") 97 | .replaceAll("_$QMARK$_", "?") 98 | .replaceAll("_$BANG$_", "!") 99 | .replaceAll("_$STAR$_", "*") 100 | .replaceAll("_$GT$_", ">") 101 | .replaceAll("_$LT$_", "<") 102 | .replaceAll("_$PLUS$_", "+") 103 | .replaceAll("_", "-") 104 | // .replaceAll("_$UNDERSCORE$_", "_") 105 | .replaceAll("_$DOLLAR$_", "$") 106 | unmunge_cache[id]=unmunged 107 | munge_cache[unmunged]=id 108 | return unmunged 109 | } 110 | 111 | export function fixed_prop(o, k, v) { 112 | // Unfortunately defineProperty has non-negligable performance implications. 113 | // We'd love to make more stuff immutable, but until browsers/js-engines 114 | // make this faster we'll like just do a simply assignment 115 | 116 | // Object.defineProperty(o, k, {value: v, writable: false, enumerable: true}) 117 | o[k]=v 118 | return o 119 | } 120 | 121 | export function fixed_props(o, kvs) { 122 | for (const k in kvs) { 123 | fixed_prop(o, k, kvs[k]) 124 | } 125 | return o 126 | } 127 | 128 | const valid_identifier_regex = /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/ 129 | const reserved_keyword_regex = /^(do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$/ 130 | 131 | export function reserved_keyword_p(n) { 132 | return typeof n === 'string' && reserved_keyword_regex.test(n) 133 | } 134 | 135 | export function valid_property_name_p(n) { 136 | return typeof n === 'string' && valid_identifier_regex.test(n) && !reserved_keyword_regex.test(n) 137 | } 138 | -------------------------------------------------------------------------------- /lib/piglet/lang/xxhash32.mjs: -------------------------------------------------------------------------------- 1 | // Extracted from the hash-wasm project, and inlined here, with minimal JS 2 | // WebAssembly interface, and modified to return a 32 bit integer rather than a 3 | // hex encoded string. 4 | 5 | // Based on: https://github.com/Daninet/hash-wasm 6 | // Revision: bd3a205ca5603fc80adf71d0966fc72e8d4fa0ef 7 | 8 | //// hash-wasm LICENSE file: 9 | // 10 | // MIT License 11 | // 12 | // Copyright (c) 2020 Dani Biró 13 | // 14 | // Permission is hereby granted, free of charge, to any person obtaining a copy 15 | // of this software and associated documentation files (the "Software"), to deal 16 | // in the Software without restriction, including without limitation the rights 17 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | // copies of the Software, and to permit persons to whom the Software is 19 | // furnished to do so, subject to the following conditions: 20 | // 21 | // The above copyright notice and this permission notice shall be included in all 22 | // copies or substantial portions of the Software. 23 | // 24 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | // SOFTWARE. 31 | // 32 | // Special thank you to the authors of original C algorithms: 33 | // - Alexander Peslyak 34 | // - Aleksey Kravchenko 35 | // - Colin Percival 36 | // - Stephan Brumme 37 | // - Steve Reid 38 | // - Samuel Neves 39 | // - Solar Designer 40 | // - Project Nayuki 41 | // - ARM Limited 42 | // - Yanbo Li dreamfly281@gmail.com, goldboar@163.comYanbo Li 43 | // - Mark Adler 44 | // - Yann Collet 45 | 46 | const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 47 | const base64Lookup = new Uint8Array(256); 48 | for (let i = 0; i < base64Chars.length; i++) { 49 | base64Lookup[base64Chars.charCodeAt(i)] = i; 50 | } 51 | 52 | function getDecodeBase64Length(data) { 53 | let bufferLength = Math.floor(data.length * 0.75); 54 | const len = data.length; 55 | if (data[len - 1] === '=') { 56 | bufferLength -= 1; 57 | if (data[len - 2] === '=') { 58 | bufferLength -= 1; 59 | } 60 | } 61 | return bufferLength; 62 | } 63 | 64 | function decodeBase64(data) { 65 | const bufferLength = getDecodeBase64Length(data); 66 | const len = data.length; 67 | const bytes = new Uint8Array(bufferLength); 68 | let p = 0; 69 | for (let i = 0; i < len; i += 4) { 70 | const encoded1 = base64Lookup[data.charCodeAt(i)]; 71 | const encoded2 = base64Lookup[data.charCodeAt(i + 1)]; 72 | const encoded3 = base64Lookup[data.charCodeAt(i + 2)]; 73 | const encoded4 = base64Lookup[data.charCodeAt(i + 3)]; 74 | bytes[p] = (encoded1 << 2) | (encoded2 >> 4); 75 | p += 1; 76 | bytes[p] = ((encoded2 & 15) << 4) | (encoded3 >> 2); 77 | p += 1; 78 | bytes[p] = ((encoded3 & 3) << 6) | (encoded4 & 63); 79 | p += 1; 80 | } 81 | return bytes; 82 | } 83 | 84 | 85 | // Additional license information for xxhash32.c, which this blob is built from: 86 | 87 | ///////////////////////////////////////////////////////////// 88 | // xxhash32.h 89 | // Copyright (c) 2016 Stephan Brumme. All rights reserved. 90 | // see http://create.stephan-brumme.com/disclaimer.html 91 | // 92 | // XXHash (32 bit), based on Yann Collet's descriptions, see 93 | // http://cyan4973.github.io/xxHash/ 94 | // 95 | // Modified for hash-wasm by Dani Biró 96 | // 97 | 98 | const wasm = decodeBase64("AGFzbQEAAAABEQRgAAF/YAF/AGAAAGACf38AAwcGAAEBAgADBAUBcAEBAQUEAQECAgYOAn8BQbCJBQt/AEGACAsHcAgGbWVtb3J5AgAOSGFzaF9HZXRCdWZmZXIAAAlIYXNoX0luaXQAAQtIYXNoX1VwZGF0ZQACCkhhc2hfRmluYWwAAw1IYXNoX0dldFN0YXRlAAQOSGFzaF9DYWxjdWxhdGUABQpTVEFURV9TSVpFAwEKswkGBQBBgAkLTQBBAEIANwOoiQFBACAANgKIiQFBACAAQc+Moo4GajYCjIkBQQAgAEH3lK+veGo2AoSJAUEAIABBqIiNoQJqNgKAiQFBAEEANgKgiQELswUBBn8CQCAARQ0AQQBBACkDqIkBIACtfDcDqIkBAkBBACgCoIkBIgEgAGpBD0sNAEEAIAFBAWo2AqCJASABQZCJAWpBAC0AgAk6AAAgAEEBRg0BQQEhAgNAQQBBACgCoIkBIgFBAWo2AqCJASABQZCJAWogAkGACWotAAA6AAAgACACQQFqIgJHDQAMAgsLIABB8AhqIQMCQAJAIAENAEEAKAKMiQEhAUEAKAKIiQEhBEEAKAKEiQEhBUEAKAKAiQEhBkGACSECDAELQYAJIQICQCABQQ9LDQBBgAkhAgNAIAItAAAhBEEAIAFBAWo2AqCJASABQZCJAWogBDoAACACQQFqIQJBACgCoIkBIgFBEEkNAAsLQQBBACgCkIkBQfeUr694bEEAKAKAiQFqQQ13QbHz3fF5bCIGNgKAiQFBAEEAKAKUiQFB95Svr3hsQQAoAoSJAWpBDXdBsfPd8XlsIgU2AoSJAUEAQQAoApiJAUH3lK+veGxBACgCiIkBakENd0Gx893xeWwiBDYCiIkBQQBBACgCnIkBQfeUr694bEEAKAKMiQFqQQ13QbHz3fF5bCIBNgKMiQELIABBgAlqIQACQCACIANLDQADQCACKAIAQfeUr694bCAGakENd0Gx893xeWwhBiACQQxqKAIAQfeUr694bCABakENd0Gx893xeWwhASACQQhqKAIAQfeUr694bCAEakENd0Gx893xeWwhBCACQQRqKAIAQfeUr694bCAFakENd0Gx893xeWwhBSACQRBqIgIgA00NAAsLQQAgATYCjIkBQQAgBDYCiIkBQQAgBTYChIkBQQAgBjYCgIkBQQAgACACayIBNgKgiQEgAUUNAEEAIQEDQCABQZCJAWogAiABai0AADoAACABQQFqIgFBACgCoIkBSQ0ACwsLzAICAX4Gf0EAKQOoiQEiAKchAQJAAkAgAEIQVA0AQQAoAoSJAUEHd0EAKAKAiQFBAXdqQQAoAoiJAUEMd2pBACgCjIkBQRJ3aiECDAELQQAoAoiJAUGxz9myAWohAgsgAiABaiECQZCJASEBQQAoAqCJASIDQZCJAWohBAJAIANBBEgNAEGQiQEhBQNAIAUoAgBBvdzKlXxsIAJqQRF3Qa/W074CbCECIAVBCGohBiAFQQRqIgEhBSAGIARNDQALCwJAIAEgBEYNACADQZCJAWohBQNAIAEtAABBsc/ZsgFsIAJqQQt3QbHz3fF5bCECIAUgAUEBaiIBRw0ACwtBACACQQ92IAJzQfeUr694bCIBQQ12IAFzQb3cypV8bCIBQRB2IAFzIgFBGHQgAUEIdEGAgPwHcXIgAUEIdkGA/gNxIAFBGHZycq03A4AJCwYAQYCJAQtTAEEAQgA3A6iJAUEAIAE2AoiJAUEAIAFBz4yijgZqNgKMiQFBACABQfeUr694ajYChIkBQQAgAUGoiI2hAmo2AoCJAUEAQQA2AqCJASAAEAIQAwsLCwEAQYAICwQwAAAA") 99 | 100 | const {instance} = await WebAssembly.instantiate(wasm) 101 | 102 | const {memory, Hash_Init, Hash_Update, Hash_Final, Hash_GetBuffer} = instance.exports 103 | 104 | /** 105 | * Hash a uint8array 106 | */ 107 | export function hash_bytes(uint8) { 108 | const mem = new Uint8Array(memory.buffer, Hash_GetBuffer(), uint8.byteLength) 109 | mem.set(uint8) 110 | Hash_Init(0) 111 | Hash_Update(uint8.byteLength) 112 | Hash_Final() 113 | return new Uint32Array(mem.buffer,mem.byteOffset,1)[0] 114 | } 115 | 116 | const text_encoder = new TextEncoder() 117 | 118 | /** 119 | * Hash a string 120 | */ 121 | export function hash_str(s) { 122 | try { 123 | const mem = new Uint8Array(memory.buffer, Hash_GetBuffer(), s.length*3) 124 | const {read} = text_encoder.encodeInto(s, mem) 125 | Hash_Init(0) 126 | Hash_Update(read) 127 | Hash_Final() 128 | return new Uint32Array(mem.buffer,mem.byteOffset,1)[0] 129 | } catch (e) { 130 | console.log("HASH_STR FAILED", s, e) 131 | } 132 | } 133 | 134 | export function hash_num(num) { 135 | return hash_bytes(new Uint8Array(new Float64Array([num]).buffer)) 136 | } 137 | 138 | // https://stackoverflow.com/a/27952689/1891448 139 | // https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine 140 | export function hash_combine(seed, hash) { 141 | return seed ^ hash + 0x9e3779b9 + (seed << 6) + (seed >> 2); 142 | } 143 | -------------------------------------------------------------------------------- /lib/piglet/node/AOTCompiler.mjs: -------------------------------------------------------------------------------- 1 | import NodeCompiler from "./NodeCompiler.mjs" 2 | 3 | class AOTCompiler extends NodeCompiler { 4 | async eval(form) { 5 | if (this.verbosity >= 2) { 6 | println("--- form ------------") 7 | println(form) 8 | } 9 | const ast = this.analyzer.analyze(form) 10 | if (this.verbosity >= 3) { 11 | println("--- AST -------------") 12 | console.dir(ast, {depth: null}) 13 | } 14 | let estree = ast.emit(this.code_gen) 15 | if (this.verbosity >= 4) { 16 | println("--- estree ----------") 17 | console.dir(estree, {depth: null}) 18 | } 19 | if (this.writer) { 20 | this.writer.write( 21 | this.estree_to_js( 22 | Array.isArray(estree) ? 23 | {type: 'Program', body: estree} : 24 | estree)) 25 | } 26 | estree = this.code_gen.wrap_async_iife(ast, estree) 27 | if (this.verbosity >= 5) { 28 | println("--- WRAPPED estree ----------") 29 | console.dir(estree, { depth: null }) 30 | } 31 | let js = this.estree_to_js(estree) 32 | if (this.verbosity >= 1) { 33 | println("--- js --------------") 34 | println(js) 35 | } 36 | return await eval(js) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /lib/piglet/node/NodeCompiler.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import * as path from "node:path" 4 | import * as fs from "node:fs" 5 | import * as url from "node:url" 6 | 7 | import * as astring from "astring" 8 | 9 | import AbstractCompiler from "../lang/AbstractCompiler.mjs" 10 | import {module_registry, deref, resolve, symbol, qname, read_string, expand_qnames} from "../lang.mjs" 11 | 12 | const pkg$name = qname('https://vocab.piglet-lang.org/package/name') 13 | const pkg$deps = qname('https://vocab.piglet-lang.org/package/deps') 14 | const pkg$location = qname('https://vocab.piglet-lang.org/package/location') 15 | const pkg$paths = qname('https://vocab.piglet-lang.org/package/location') 16 | 17 | export default class NodeCompiler extends AbstractCompiler { 18 | async slurp(path) { 19 | return fs.readFileSync(path).toString() 20 | } 21 | 22 | async spit(path, content) { 23 | return fs.writeFileSync(path, content) 24 | } 25 | 26 | async mkdir_p(path) { 27 | fs.mkdirSync(path, { recursive: true }) 28 | } 29 | 30 | async slurp_mod(pkg_name, mod_name) { 31 | const pkg = module_registry.find_package(pkg_name) 32 | if (!pkg) { 33 | throw new Error(`No such package present: ${pkg_name}`) 34 | } 35 | for (let dir of pkg.paths) { 36 | const mod_path = url.fileURLToPath(new URL(`${mod_name}.pig`, new URL(`${dir}/`, `${pkg.location}/`))) 37 | if (fs.existsSync(mod_path)) { 38 | return [fs.readFileSync(mod_path), mod_path] 39 | } 40 | } 41 | throw new Error(`Module not found: ${mod_name} in ${pkg.inspect()}`) 42 | } 43 | 44 | resolve_js_path(js_path) { 45 | if (js_path.startsWith("node:")) return js_path 46 | 47 | const mod = deref(resolve(symbol("piglet:lang:*current-module*"))) 48 | let base_path = mod.location 49 | 50 | // FIXME: this returns absolute file URLs, which is fine in direct mode but 51 | // not in AOT mode, where we need to generate something that the JS runtime 52 | // (node) can resolve. 53 | if (js_path.startsWith("./") || js_path.startsWith("../")) { 54 | return new URL(js_path, `${url.pathToFileURL(base_path)}`).href 55 | } 56 | 57 | // FIXME: handle "exports" 58 | while (true) { 59 | const module_path = path.join(base_path, "node_modules", js_path) 60 | if (fs.existsSync(module_path)) { 61 | const package_json = JSON.parse(fs.readFileSync(path.join(module_path, "package.json"), 'utf8')) 62 | // FIXME, hard coding some cases right now, we need to redo this 63 | // properly and fix the code duplication with dev-server 64 | let resolve_default = (o)=>typeof o === 'string'?o:o.default 65 | if (package_json.exports?.["."]?.import) { 66 | return path.join(module_path, resolve_default(package_json.exports?.["."]?.import)) 67 | } 68 | if (package_json.exports?.default) { 69 | return path.join(module_path, package_json.exports?.default) 70 | } 71 | if (package_json.main) return path.join(module_path, package_json.main) 72 | return path.join(module_path, "index.js") 73 | } 74 | base_path = path.join(base_path, '..') 75 | if (base_path === "/") return null 76 | } 77 | } 78 | 79 | estree_to_js(estree) { 80 | if (this.source_map_generator) { 81 | return astring.generate(estree, {comments: true, sourceMap: this.source_map_generator}) 82 | } 83 | return astring.generate(estree, {comments: true}) 84 | } 85 | 86 | async load_package(location) { 87 | const package_pig_loc = path.join(location, "package.pig") 88 | if (fs.existsSync(package_pig_loc)) { 89 | let package_pig = expand_qnames(read_string(await this.slurp(package_pig_loc))) 90 | return this.register_package(url.pathToFileURL(location).href, package_pig) 91 | } else { 92 | console.log(`WARN: no package.pig found at ${package_pig_loc}`) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/piglet/node/NodeREPL.mjs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 2 | 3 | import process, {stdin, stdout} from 'node:process' 4 | import * as readline from 'node:readline' 5 | import * as path from 'node:path' 6 | import * as fs from 'node:fs' 7 | import * as os from 'node:os' 8 | import * as astring from 'astring' 9 | 10 | import {resolve, println, prn, symbol, intern, inspect, string_reader} from "../lang.mjs" 11 | import {PartialParse} from "../lang/StringReader.mjs" 12 | 13 | export default class NodeREPL { 14 | constructor(compiler, opts) { 15 | this.compiler = compiler 16 | this.analyzer = compiler.analyzer 17 | this.code_gen = compiler.code_gen 18 | this.reader = string_reader("") 19 | this.verbosity = opts?.verbosity || 0 20 | } 21 | 22 | prompt() { 23 | const mod = resolve(symbol("piglet:lang:*current-module*")).deref() 24 | return `${mod.fqn.toString().replace("https://piglet-lang.org/packages/", "")}=> ` 25 | } 26 | 27 | write_prompt() { 28 | stdout.write(this.prompt()) 29 | } 30 | 31 | eval_more() { 32 | let start_pos = this.reader.pos 33 | try { 34 | let form = this.reader.read() 35 | let result = this.compiler.eval(form) 36 | return result.then((v)=>{ 37 | prn(v) 38 | intern(symbol("*3"), resolve(symbol("*2"))?.deref()) 39 | intern(symbol("*2"), resolve(symbol("*1"))?.deref()) 40 | intern(symbol("*1"), v) 41 | try {this.reader.skip_ws()} catch (_) {} 42 | this.reader.truncate() 43 | if (!this.reader.eof()) { 44 | return this.eval_more() 45 | } else { 46 | this.write_prompt() 47 | } 48 | }, (e)=> { 49 | intern(symbol("user:*e"), e) 50 | console.log(e) 51 | try {this.reader.skip_ws()} catch (_) {} 52 | this.reader.truncate() 53 | if (!this.reader.eof()) { 54 | return this.eval_more() 55 | } else { 56 | this.write_prompt() 57 | } 58 | }) 59 | } catch (e) { 60 | if (e instanceof PartialParse) { 61 | this.reader.pos = start_pos 62 | } else { 63 | intern(symbol("user:*e"), e) 64 | console.log(e) 65 | this.reader.empty() 66 | this.write_prompt() 67 | } 68 | } 69 | } 70 | 71 | eval(data) { 72 | try { 73 | const input = data.toString() 74 | this.reader.append(input) 75 | if (!this.reader.eof()) { 76 | this.eval_more() 77 | } 78 | } catch (e) { 79 | intern(symbol("user:*e"), e) 80 | console.log(e) 81 | this.reader.empty() 82 | this.write_prompt() 83 | } 84 | } 85 | 86 | start() { 87 | process.on( 88 | 'unhandledRejection', 89 | (reason, promise) => { 90 | // promise.then(null, (e)=> intern(symbol("user:*e", e))) 91 | console.log('Unhandled Rejection at:', promise, 'reason:', reason) 92 | } 93 | ) 94 | this.write_prompt() 95 | stdin.on('data', this.eval.bind(this)) 96 | stdin.on('end', ()=>process.exit()) 97 | } 98 | 99 | start_readline() { 100 | process.on( 101 | 'unhandledRejection', 102 | (reason, promise) => { 103 | // promise.then(null, (e)=> intern(symbol("user:*e", e))) 104 | console.log('Unhandled Rejection at:', promise, 'reason:', reason) 105 | } 106 | ) 107 | 108 | const pigletDir = path.join(os.homedir(), '.local/piglet') 109 | const historyFile = path.join(pigletDir, 'repl_history') 110 | fs.mkdirSync(pigletDir, { recursive: true }) 111 | if(!fs.existsSync(historyFile)) fs.writeFileSync(historyFile, "") 112 | const history = fs.readFileSync(historyFile, 'utf-8').split('\n') 113 | 114 | const rl = readline.createInterface({ 115 | input: process.stdin, 116 | output: process.stdout, 117 | prompt: this.prompt(), 118 | history: history 119 | }); 120 | 121 | rl.prompt() 122 | 123 | rl.on('line', (line) => { 124 | this.eval(line) 125 | rl.setPrompt(this.prompt()) 126 | }).on('close', () => { 127 | fs.writeFileSync(historyFile, rl.history.join('\n')) 128 | console.log('\n\n\x1B[31m n n') 129 | console.log(' (・⚇・)/ \x1B[33mgoodbye!\x1B[31m') 130 | console.log(' ~(_ _) \x1B[33m...oink\x1B[0m') 131 | process.exit(0); 132 | }) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/piglet/node/main.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 4 | 5 | Error.stackTraceLimit = Infinity; 6 | 7 | import * as process from "node:process" 8 | import * as fs from "node:fs" 9 | import * as url from "node:url" 10 | import * as path from "node:path" 11 | import { createRequire } from 'node:module' 12 | 13 | import NodeCompiler from "./NodeCompiler.mjs" 14 | import PrefixName from "../lang/PrefixName.mjs" 15 | import NodeREPL from "./NodeREPL.mjs" 16 | 17 | import {read_string, intern, resolve, deref, 18 | symbol, keyword, prefix_name, 19 | qname, qsym, dict, 20 | module_registry} from "../lang.mjs" 21 | 22 | import {PIGLET_PKG} from "../lang/util.mjs" 23 | 24 | global.$piglet$ = module_registry.index 25 | 26 | // Currently aiming for compatibility with Node.js 17+, hence no util.parseArgs 27 | 28 | let verbosity = 0, evals = [], imports = [], packages = [], positionals = [] 29 | 30 | for (let idx = 2 ; idx < process.argv.length; idx++) { 31 | switch (process.argv[idx]) { 32 | case "-e": 33 | case "--eval": 34 | idx+=1 35 | evals.push(process.argv[idx]) 36 | break; 37 | case "-i": 38 | case "--import": 39 | idx+=1 40 | imports.push(process.argv[idx]) 41 | break; 42 | case "-p": 43 | case "--package": 44 | idx+=1 45 | packages.push(process.argv[idx]) 46 | break; 47 | default: 48 | if (/-v+/.test(process.argv[idx])) { 49 | verbosity += process.argv[idx].length-1 50 | break; 51 | } 52 | positionals.push(process.argv[idx]) 53 | } 54 | } 55 | 56 | 57 | // console.log({verbosity, evals, imports, packages, positionals}) 58 | 59 | const compiler = new NodeCompiler() 60 | 61 | // defined as a constant so it's easy to replace when using a build system 62 | // const PIGLET_PACKAGE_PATH = path.join(url.fileURLToPath(import.meta.url), "../packages/piglet") 63 | const PIGLET_PACKAGE_PATH = path.join(url.fileURLToPath(import.meta.url), "../../../../packages/piglet") 64 | 65 | await compiler.load_package(PIGLET_PACKAGE_PATH); 66 | 67 | intern(symbol('piglet:lang:*compiler*'), compiler) 68 | intern(symbol('piglet:lang:*verbosity*'), verbosity) 69 | await compiler.load(qsym(`${PIGLET_PKG}:lang`)) 70 | 71 | let local_pkg = null 72 | if (fs.existsSync("./package.pig")) { 73 | const pkg = await compiler.load_package(".") 74 | local_pkg = pkg.name 75 | compiler.set_current_package(local_pkg) 76 | } 77 | 78 | if (packages) { 79 | for (const p of packages) { 80 | if (local_pkg) compiler.set_current_package(local_pkg) 81 | await compiler.load_package(p) 82 | } 83 | } 84 | 85 | if (imports) { 86 | for (const m of imports) { 87 | if (local_pkg) compiler.set_current_package(local_pkg) 88 | await compiler.load(symbol(m)) 89 | } 90 | } 91 | 92 | if (evals.length > 0) { 93 | for (const e of evals) { 94 | console.log(e) 95 | await compiler.eval_string(e) 96 | } 97 | } 98 | 99 | if (positionals.length > 0) { 100 | const file = positionals[0] 101 | if (local_pkg) compiler.set_current_package(local_pkg) 102 | await compiler.load_file(path.resolve(process.cwd(), file)) 103 | } 104 | 105 | 106 | if (positionals.length === 0 && evals.length === 0) { 107 | new NodeREPL(compiler, {verbosity}).start() 108 | } 109 | -------------------------------------------------------------------------------- /lib/piglet/node/pig_cli.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright (c) Arne Brasseur 2023. All rights reserved. 4 | 5 | // Bootstrap piglet, then launch the "pig" CLI tool 6 | 7 | import * as process from "node:process" 8 | import * as url from "node:url" 9 | import * as path from "node:path" 10 | import NodeCompiler from "./NodeCompiler.mjs" 11 | import {intern, symbol, qsym, module_registry} from "../lang.mjs" 12 | import {PIGLET_PKG} from "../lang/util.mjs" 13 | 14 | global.$piglet$ = module_registry.index 15 | 16 | const compiler = new NodeCompiler() 17 | 18 | const PIGLET_PACKAGE_PATH = path.join(url.fileURLToPath(import.meta.url), "../../../../packages/piglet") 19 | await compiler.load_package(PIGLET_PACKAGE_PATH); 20 | intern(symbol('piglet:lang:*compiler*'), compiler) 21 | intern(symbol('piglet:lang:*verbosity*'), 0) 22 | 23 | await compiler.load(qsym(`${PIGLET_PKG}:lang`)) 24 | await compiler.load(qsym(`${PIGLET_PKG}:node/pig-cli`)) 25 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | Vision 2 | 3 | - Compile LISP to JavaScript AST (estree) 4 | - Leave emitting of JS, minimizing, etc. to other tools (astring, escodegen, esmangle) 5 | - Core is pure ES6, run in Node or browser (or elsewhere) directly from source 6 | - Introduce a URI type that quacks like a keyword (can do map lookup etc) 7 | - :foo:bar syntax resolves to full URI based on project/module config of prefixes (a la JSON-LD) 8 | - contains? -> contains-key? 9 | - persistent data types, but pluggable (protocol driven) 10 | - nrepl-ish facilities built-in, but based on websockets/funnel 11 | 12 | 13 | Three strategies? 14 | 15 | - ESM 16 | - globals 17 | - dynamic imports? 18 | 19 | FQID (fully qualified ids) 20 | 21 | - :foo -> regular keyword, not an FQID 22 | - :foo/bar/baz -> slash is not a special character, can have as many as you want 23 | 24 | - :https://foo.bar/baz 25 | - :foo.bar:baz 26 | - ::baz 27 | 28 | -> FQID starts with a `:` and contains one (1!) other `:` 29 | -> if it contains `://` then it is fully qualified, no expansion needed 30 | -> Otherwise the part before `:` is looked up in the *context* 31 | 32 | - :"foo:bar is great!!!" 33 | -> additional double quotes are allowed, and allow a wider range of characters 34 | 35 | 36 | (module foo/bar 37 | (:context {"foo.bar" "https://foo.bar?"})) 38 | 39 | :foo.bar:baz -> :"https://foo.bar?baz" 40 | -> first part is looked up in context and used for expansion 41 | 42 | (module foo/bar 43 | (:context {"foo.bar" "https://foo.bar/" 44 | "baz" "foo.bar:baz" ;;-> can itself contain a `:` which is expanded based on the context} 45 | 46 | ::baz -> :https://foo.bar/baz 47 | -> with this syntax you look up `baz` by itself in the context, this becomes the expanded value 48 | 49 | ::moo -> :bunnny-lang://project-name/foo/bar/moo 50 | -> if "moo" is not present in the context, then this becomes an FQID based on the project and module name 51 | -> exact URL format is very much TBD 52 | -> this implies there is a notion of a "current" URL that is used as the basis for expansion (kind of like relative links in HTML) 53 | 54 | 55 | -> so can we also use relative references (not sure yet about this last bit) 56 | 57 | (module foo/bar 58 | (:context {"moo" "../moo"})) 59 | 60 | ::moo -> :piglet-lang://package-name/foo/moo 61 | 62 | Use within a given module is always static, using the context from the module 63 | (and possibly parent modules, and the top-level package), so if a function 64 | contains a relative reference like ::moo, then this will be locked in at compile 65 | time. 66 | 67 | But there are also facilities for dynamically accessing the context, in 68 | particular for printing. So a context would be kept based on the currently 69 | evaluating module, and printing an identifier would compact it based on that 70 | context. 71 | 72 | ---------------------------------------------------------------------------- 73 | 74 | 75 | (module foo/bar/baz 76 | (:import ...) 77 | (:context {...}) 78 | 79 | JS imports: 80 | 81 | [left-pad :from :npm:left-pad] 82 | -> import left_pad from "left-pad" 83 | 84 | [foo :from :node:process] 85 | -> import foo from "node:process" 86 | 87 | [foo :from :js:./foo/bar.js] 88 | -> import foo from "./foo/bar.js" 89 | 90 | Or maybe just use strings and pass through? 91 | 92 | [foo :from "./foo/bar.js"] 93 | [foo :from "node:fs"] 94 | 95 | bun imports: 96 | [n :from :lambdaisland/uri:normalize] 97 | import n from "../../../lambdaisland~uri/normalize.mjs" 98 | 99 | (n:normalize ...) 100 | 101 | 102 | [foo :from ::my/local/file] 103 | import normalize from "../../../user/my/local/file.mjs" 104 | 105 | ::foo 106 | :localpackage:foo 107 | "./foo" 108 | 109 | [b :from :piglet:lang] 110 | 111 | (b:str) 112 | (piglet:lang:str) 113 | 114 | Symbols: 115 | package:module:var 116 | 117 | Imports 118 | ::module (= :localpkg:module) 119 | :package:module 120 | :special-prefix:module 121 | 122 | BUGS (to be logged) 123 | 124 | - Protocol methods are missing location metadata 125 | - Calling a protocol method within a protocol method definition calls the 126 | concrete implementation, instead of dispatching via the protocol. 127 | - This destructuring doesn't seem to work in macros: (defmacro foo [[x & xs] & rest] ) 128 | 129 | 130 | ----- 131 | 132 | JS Imports 133 | 134 | - relative ./ ../ 135 | - directly relative to the location of the .pig file 136 | - resolved 137 | - "package-name" -> node resolution rules 138 | - "package-name:file/in/package.js" -> FS resolution from `node_modules/package_name` 139 | - piglet-package-alias:foo/bar.js -> loads JS file from piglet package alias 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "piglet-lang", 3 | "version": "0.1.42", 4 | "homepage": "", 5 | "author": { 6 | "name": "Arne Brasseur", 7 | "url": "https://github.com/plexus" 8 | }, 9 | "engines": { 10 | "node": ">=18" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/piglet-lang/piglet.git" 15 | }, 16 | "description": "LISP compiler and runtime", 17 | "dependencies": { 18 | "acorn-class-fields": "^1.0.0", 19 | "astring": "^1.9.0", 20 | "express": "^4.18.2", 21 | "jsdom": "^22.1.0", 22 | "mime-db": "^1.52.0", 23 | "p5": "^1.6.0", 24 | "solid-js": "^1.7.5", 25 | "source-map": "^0.7.4", 26 | "ws": "^8.13.0" 27 | }, 28 | "devDependencies": { 29 | "@rollup/plugin-node-resolve": "^15.2.2", 30 | "@rollup/plugin-replace": "^5.0.3", 31 | "@rollup/plugin-terser": "^0.4.4", 32 | "@rollup/pluginutils": "^5.0.5", 33 | "acorn": "^8.14.1", 34 | "nodemon": "^2.0.21", 35 | "readline-history": "^1.2.0", 36 | "rollup": "^4.0.2", 37 | "tiny-source-map": "^0.8.0" 38 | }, 39 | "bin": { 40 | "pig": "bin/pig", 41 | "piglet": "bin/piglet", 42 | "piglet-lang": "bin/piglet" 43 | }, 44 | "exports": { 45 | ".": { 46 | "node": "./dist/piglet.node.mjs", 47 | "module": "./dist/piglet.browser.mjs" 48 | }, 49 | "./*": "./lib/piglet/lang/*" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/demo/express_test.pig: -------------------------------------------------------------------------------- 1 | (module express-test 2 | (:import 3 | hiccup 4 | [chalk :from "chalk"] 5 | [express :from "express"])) 6 | 7 | (def port 3005) 8 | 9 | (println express) 10 | (println express:default) 11 | 12 | (def app (express:default)) 13 | 14 | (def routes 15 | [[:get "/" (fn [req] 16 | {:status 200 17 | :headers {"content-type" "text/html"} 18 | :body (hiccup:html [:p "Hello, world"])})]]) 19 | 20 | (.get app "/" 21 | (fn [req res] 22 | (.send res (hiccup:html [:p "Hello, world"])))) 23 | 24 | 25 | (println (.blue chalk:default "Starting Express.js on port") (.red chalk:default port)) 26 | (.listen app port) 27 | -------------------------------------------------------------------------------- /packages/demo/hiccup.pig: -------------------------------------------------------------------------------- 1 | (module ::hiccup) 2 | 3 | (defn html [arg] 4 | (if (string? arg) 5 | arg 6 | (if (sequential? arg) 7 | (let [[tag props] arg 8 | children (if (dict? props) (rest (rest arg)) (rest arg)) 9 | props (if (dict? props) props {})] 10 | (str "<" (name tag) ">" (apply str (map html children)) 11 | ""))))) 12 | -------------------------------------------------------------------------------- /packages/demo/package.pig: -------------------------------------------------------------------------------- 1 | {:pkg:name https://my-project.org/packages/hello-world 2 | :pkg:paths ["."] 3 | :pkg:deps {} 4 | :pkg:main ::demo 5 | } 6 | -------------------------------------------------------------------------------- /packages/dev-server/package.pig: -------------------------------------------------------------------------------- 1 | {:pkg:name https://piglet-lang.org/packages/dev-server 2 | :pkg:paths ["src"]} 3 | -------------------------------------------------------------------------------- /packages/nrepl/package.pig: -------------------------------------------------------------------------------- 1 | {:pkg:id :https://piglet-lang.org/pkg/nrepl 2 | :pkg:paths ["src"] 3 | :pkg:npm-deps {bencode "^3.0.3"} 4 | :pkg:context {}} 5 | -------------------------------------------------------------------------------- /packages/nrepl/src/nrepl.pig: -------------------------------------------------------------------------------- 1 | (module :nrepl:nrepl 2 | (:import 3 | [bencode :from "bencode"] 4 | [net :from "node:net"])) 5 | 6 | ;;(in-mod ':nrepl:nrepl) 7 | 8 | (def sessions {}) 9 | 10 | (defn pig->js [o] 11 | (cond 12 | (keyword? o) 13 | (name o) 14 | 15 | (symbol? o) 16 | (-repr o) 17 | 18 | (qname? o) 19 | (.-fqn o) 20 | 21 | (prefix-name? o) 22 | (-repr o) 23 | 24 | (dict? o) 25 | (reduce 26 | (fn [acc [k v]] 27 | (conj! acc [(pig->js k) (pig->js v)])) 28 | (js:Object) o) 29 | 30 | (sequential? o) 31 | (js:Array.from o pig->js) 32 | 33 | :else o)) 34 | 35 | (defn js-obj [& kvs] 36 | (reduce 37 | (fn [o [k v]] 38 | (conj! o [(name k) v])) 39 | (js:Object) 40 | (partition 2 kvs))) 41 | 42 | (defn bencode [data] 43 | ((.-encode bencode:default) (pig->js data))) 44 | 45 | (defn bdecode [data] 46 | ((.-decode bencode:default) data "utf-8")) 47 | 48 | (defn response-for [old-msg msg] 49 | (js:Object.assign 50 | (js-obj :id (.-id old-msg) :session (.-session old-msg)) 51 | msg)) 52 | 53 | (defn send-msg [conn msg] 54 | (println '<- msg) 55 | (.write conn (bencode msg))) 56 | 57 | (defn op-clone [conn msg] 58 | (let [id (str (inc (count sessions)))] 59 | (.set_value (resolve 'nrepl:nrepl:sessions) (assoc sessions id {})) 60 | (send-msg conn (response-for msg (js-obj "new-session" id "status" ["done"]))))) 61 | 62 | (defn op-describe [conn msg] 63 | (send-msg 64 | conn 65 | (response-for 66 | msg 67 | (js-obj "status" ["done"] 68 | "aux" {} 69 | "ops" {"clone" {} "describe" {} "eval" {}})))) 70 | 71 | (defn ^:async op-eval [conn msg] 72 | (let [code (.-code msg) 73 | form (read-string code) 74 | result-p (eval form)] 75 | (.then result-p 76 | (fn [result] 77 | (send-msg 78 | conn 79 | (response-for 80 | msg 81 | (js-obj 82 | "ns" (-repr *current-module*) 83 | "value" (print-str result)))) 84 | (send-msg 85 | conn 86 | (response-for 87 | msg 88 | (js-obj "status" ["done"])))) 89 | (fn [error] 90 | (send-msg 91 | conn 92 | (response-for 93 | msg 94 | (js-obj 95 | "ns" (-repr *current-module*) 96 | "value" (print-str error)))) 97 | (send-msg 98 | conn 99 | (response-for 100 | msg 101 | (js-obj "status" ["done"]))))))) 102 | 103 | 104 | (defn ^:async op-load-file [conn msg] 105 | (let [code (.-file msg) 106 | result-p (.eval_string *compiler* code)] 107 | (.then result-p 108 | (fn [result] 109 | (send-msg 110 | conn 111 | (response-for 112 | msg 113 | (js-obj 114 | "ns" (-repr *current-module*) 115 | "value" (print-str result)))) 116 | (send-msg 117 | conn 118 | (response-for 119 | msg 120 | (js-obj "status" ["done"])))) 121 | (fn [error] 122 | (send-msg 123 | conn 124 | (response-for 125 | msg 126 | (js-obj 127 | "ns" (-repr *current-module*) 128 | "value" (print-str error)))) 129 | (send-msg 130 | conn 131 | (response-for 132 | msg 133 | (js-obj "status" ["done"]))))))) 134 | 135 | (defn handle-data [conn data] 136 | (let [msg (bdecode data) 137 | op (when msg (.-op msg))] 138 | (println '-> msg) 139 | (cond 140 | (= op "clone") (op-clone conn msg) 141 | (= op "describe") (op-describe conn msg) 142 | (= op "eval") (op-eval conn msg) 143 | (= op "load-file") (op-load-file conn msg) 144 | :else 145 | (send-msg 146 | conn 147 | (response-for 148 | msg 149 | (js-obj "status" ["done"])))))) 150 | 151 | (defn handle-connection [conn] 152 | (println "New connection from" (.-remoteAddress conn) (.-remotePort conn)) 153 | (.on conn "data" (fn [data] (handle-data conn data)))) 154 | 155 | (defn start! [port] 156 | (let [server (net:createServer)] 157 | (.on server "connection" handle-connection) 158 | (.listen server port (fn [s] (println "Server listenining on" (.address server)))))) 159 | -------------------------------------------------------------------------------- /packages/piglet/package.pig: -------------------------------------------------------------------------------- 1 | {:pkg:name https://piglet-lang.org/packages/piglet 2 | :pkg:version "0.0.1" 3 | :pkg:paths ["src"]} 4 | -------------------------------------------------------------------------------- /packages/piglet/src/cli/terminal.pig: -------------------------------------------------------------------------------- 1 | (module cli/terminal 2 | "Generate output for VT100/xterm style terminals 3 | 4 | This is a minimal, fairly naive API that handles common use cases, based on 5 | the largest common denominator of terminal emulator escape code support, not 6 | an attempt to support every escape code and edge case of every terminal 7 | emulator." 8 | (:import 9 | string)) 10 | 11 | (def basic-colors 12 | {:black 0 13 | :red 1 14 | :green 2 15 | :yellow 3 16 | :blue 4 17 | :magenta 5 18 | :cyan 6 19 | :white 7}) 20 | 21 | (defn fg 22 | "Foreground color 23 | Add escape codes to the given strings so they are printed in a certain 24 | foreground color. For color use eithe a keyword (see [[basic-colors]]), 25 | or a 3-integer vector (rgb)." 26 | [color & strs] 27 | (str "\u001B[" 28 | (if (keyword? color) 29 | (+ 30 (get basic-colors color)) 30 | (str "38;2;" (string:join ";" color))) "m" 31 | (apply str strs) "\u001B[0m")) 32 | 33 | (defn bg 34 | "Background color 35 | Add escape codes to the given strings so they are printed with a certain 36 | background color. For color use eithe a keyword (see [[basic-colors]]), 37 | or a 3-integer vector (rgb)." 38 | [color & strs] 39 | (str "\u001B[" 40 | (if (keyword? color) 41 | (+ 40 (get basic-colors color)) 42 | (str "48;2;" (string:join ";" color))) 43 | "m" (apply str strs) "\u001B[0m")) 44 | -------------------------------------------------------------------------------- /packages/piglet/src/css.pig: -------------------------------------------------------------------------------- 1 | (module css 2 | (:import [str :from piglet:string])) 3 | 4 | (defprotocol Selector 5 | (selector-str [o] "Turn into a string")) 6 | 7 | (defprotocol Attr 8 | (attr-str [o] "Turn into a string")) 9 | 10 | (defprotocol Value 11 | (value-str [o] "Turn into a string")) 12 | 13 | (extend-protocol Selector 14 | js:String 15 | (selector-str [s] s) 16 | js:Array 17 | (selector-str [s] (str:join " " (map selector-str s))) 18 | Keyword 19 | (selector-str [k] (name k))) 20 | 21 | (extend-protocol Attr 22 | js:String 23 | (attr-str [s] s) 24 | Keyword 25 | (attr-str [k] (name k))) 26 | 27 | (extend-protocol Value 28 | js:String 29 | (value-str [s] s) 30 | Keyword 31 | (value-str [k] (name k)) 32 | js:Object 33 | (value-str [o] (str o))) 34 | 35 | (defn render-attrs [attrs] 36 | (str "{" 37 | (str:join ";" (map (fn [[k v]] (str (attr-str k) ":" (value-str v))) attrs)) 38 | "}")) 39 | 40 | (declare css*) 41 | 42 | (defn css* [[x & xs :as v]] 43 | (cond 44 | (vector? x) 45 | (reduce into (map css* v)) 46 | 47 | (set? x) 48 | (reduce into (map #(css* (into [%] xs)) x)) 49 | 50 | (or (string? x) (keyword? x)) 51 | (let [sel (selector-str x)] 52 | (reduce 53 | (fn [acc r] 54 | (cond-> acc 55 | (dict? r) 56 | (conj [sel (render-attrs r)]) 57 | (vector? r) 58 | (into (map (fn [[t a]] 59 | [(str sel (if (#{">"} (first t)) "" " ") t) a]) 60 | (css* r))))) 61 | [] 62 | xs)))) 63 | 64 | (defn css [s] 65 | (str:join "\n" 66 | (map #(str:join " " %) (css* s)))) 67 | 68 | (comment 69 | (css [:a {:color "green"}]) 70 | (css [:a 71 | [:em {:font-weight 800 :text-decoration "underline"}] 72 | [:span {:color "green"}]]) 73 | 74 | (css [[:main [#{:div :nav} 75 | {:color "green"}]] 76 | [:li {:margin "1em 0"}]]) 77 | (css [:div [:>span {:color "red"}]]) 78 | (css [:.foo [:.bar {:color "red"}]])) 79 | -------------------------------------------------------------------------------- /packages/piglet/src/dom.pig: -------------------------------------------------------------------------------- 1 | (module dom 2 | "Wrapper around the browser DOM API" 3 | (:import 4 | piglet:css 5 | [str :from piglet:string])) 6 | 7 | (def ^:dynamic *kebab-prefixes* ["data-" "aria-"]) 8 | 9 | (defn extend-interfaces! [window] 10 | (extend-type (.-Node window) 11 | MutableCollection 12 | (-conj! [parent child] 13 | (.appendChild parent child) 14 | parent))) 15 | 16 | (defn create-el 17 | ([doc tag] 18 | (.createElement doc (name tag))) 19 | ([doc xmlns tag] 20 | (.createElementNS doc xmlns (name tag)))) 21 | 22 | (defn fragment [doc els] 23 | (let [fragment (.createDocumentFragment doc)] 24 | (doseq [el els] 25 | (.appendChild fragment el)) 26 | fragment)) 27 | 28 | (defn text-node [doc text] 29 | (.createTextNode doc text)) 30 | 31 | (defn comment [doc text] 32 | (.createComment doc text)) 33 | 34 | (defn el-by-id [doc id] 35 | (.getElementById doc id)) 36 | 37 | (defn query-one 38 | ([qry] 39 | (query-one js:document qry)) 40 | ([el qry] 41 | (.querySelector el (css:selector-str qry)))) 42 | 43 | (defn query-all 44 | ([qry] 45 | (query-all js:document qry)) 46 | ([el qry] 47 | (.querySelectorAll el (css:selector-str qry)))) 48 | 49 | (defn attr-name [k] 50 | (let [n (name k)] 51 | (if (some #(str:starts-with? n %) *kebab-prefixes*) 52 | n 53 | (str:replace n #"-" "")))) 54 | 55 | (defn set-attr [el k v] 56 | (cond 57 | (and (= :style k) (dict? v)) 58 | (doseq [[prop val] v] 59 | (.setProperty (.-style el) (name prop) val)) 60 | 61 | (and (= :class k) (vector? v)) 62 | (set! (.-classList el) (str:join " " v)) 63 | 64 | (fn? v) 65 | (set! (oget el (attr-name k)) v) 66 | 67 | :else 68 | (.setAttribute el (attr-name k) v)) 69 | el) 70 | 71 | (defn set-attrs [el kvs] 72 | (doseq [[k v] kvs] 73 | (set-attr el k v)) 74 | el) 75 | 76 | (defn attr [el k] 77 | (.getAttribute el (name k))) 78 | 79 | (defn parent [el] (.-parentElement el)) 80 | (defn children [el] (.-children el)) 81 | (defn child-nodes [el] (.-childNodes el)) 82 | (defn first-child [el] (.-firstChild el)) 83 | (defn last-child [el] (.-lastChild el)) 84 | (defn first-el-child [el] (.-firstElementChild el)) 85 | (defn last-el-child [el] (.-lastElementChild el)) 86 | (defn next-sibling [el] (.-nextSibling el)) 87 | 88 | (defn inner-html [el] (.-innerHTML el)) 89 | (defn outer-html [el] (.-outerHTML el)) 90 | 91 | (defn append-child [el child] (.appendChild el child) el) 92 | (defn append [el & children] (apply (.bind (.-append el) el) children) el) 93 | (defn prepend [el & children] (apply (.bind (.-prepend el) el) children) el) 94 | 95 | (defn remove [el] (.remove el)) 96 | 97 | (defn split-tag [tag] 98 | (let [tag-str (or (.-suffix tag) (name tag)) 99 | tag-name (re-find "[^#\\.]+" tag-str) 100 | id (re-find "[#][^#\\.]+" tag-str) ;; currently not supported in the reader for keywords, works for strings 101 | kls (re-seq "[\\.][^#\\.]+" tag-str)] 102 | 103 | [(.-base tag) 104 | tag-name 105 | (when id (.substring id 1)) 106 | (mapv (fn [s] (.substring s 1)) kls)])) 107 | 108 | ;; FIXME (defn spit-el [[tag & tail]] ,,,) 109 | (defn split-el [form] 110 | (let [tag (first form) 111 | tail (rest form) 112 | [tag-ns tag id kls] (split-tag tag)] 113 | [(or tag-ns nil) 114 | tag 115 | (cond-> (if (dict? (first tail)) 116 | (first tail) 117 | {}) 118 | id 119 | (assoc :id id) 120 | (seq kls) 121 | (update :class (fn [class-prop] 122 | (if (vector? class-prop) 123 | (into kls class-prop) 124 | (conj kls class-prop))))) 125 | (if (dict? (first tail)) 126 | (rest tail) 127 | tail)])) 128 | 129 | (defn dom 130 | ([form] 131 | (dom js:window.document form)) 132 | ([doc form] 133 | (cond 134 | (and (object? form) (.-nodeType form)) ;; quacks like a Node 135 | form 136 | 137 | (or (string? form) (number? form)) 138 | (.createTextNode doc (str form)) 139 | 140 | (vector? form) 141 | (cond 142 | (= :<> (first form)) 143 | (dom doc (rest form)) 144 | 145 | (or 146 | (keyword? (first form)) 147 | (qname? (first form))) 148 | (let [[tag-ns tag attrs children] (split-el form) 149 | el (if tag-ns 150 | (create-el doc tag-ns tag) 151 | (create-el doc tag))] 152 | (set-attrs el attrs) 153 | (when (seq children) 154 | (doseq [c children] 155 | (append el (dom doc c)))) 156 | el) 157 | 158 | (fn? (first form)) 159 | (dom doc (apply (first form) (rest form)))) 160 | 161 | (sequential? form) 162 | (fragment doc (map (partial dom doc) form)) 163 | 164 | :else 165 | (dom doc (str form))))) 166 | 167 | (defonce LISTENERS (js:Symbol (str `LISTENERS))) 168 | 169 | (defn listen! [el k evt f] 170 | (when (not (get el LISTENERS)) 171 | (assoc! el LISTENERS (box {}))) 172 | (let [listeners (get el LISTENERS)] 173 | (when-let [l (get-in @listeners [k evt])] 174 | (.removeEventListener el evt k)) 175 | (swap! listeners assoc-in [k evt] f) 176 | (.addEventListener el evt f))) 177 | 178 | (defn unlisten! [el k evt] 179 | (let [listeners (get el LISTENERS)] 180 | (when-let [l (get-in @listeners [k evt])] 181 | (.removeEventListener el evt k) 182 | (swap! listeners update k dissoc evt)))) 183 | -------------------------------------------------------------------------------- /packages/piglet/src/node/http-server.pig: -------------------------------------------------------------------------------- 1 | (module node/http-server 2 | (:import 3 | [http :from "node:http"])) 4 | 5 | (defprotocol LifeCycle 6 | (start! [server]) 7 | (stop! [server])) 8 | 9 | (defn as-promise [v] 10 | (if (.-then v) 11 | v 12 | (js:Promise. (fn [res rej] (res v))))) 13 | 14 | (defn create-server [handler opts] 15 | (specify! 16 | (http:createServer 17 | (fn [req res] 18 | (let [url (js:URL. (.-url req) "http://example.com") ;; js:URL does not like relative 19 | response (handler {:method (.-method req) 20 | :path (.-pathname url) 21 | :query-params (into {} (.-searchParams url)) 22 | :headers (into {} 23 | (map (fn [[k v]] 24 | [(.toLowerCase k) v]) 25 | (partition 2 (.-rawHeaders req))))})] 26 | (.then (as-promise response) 27 | (fn [response] 28 | ;; (println 'http-> response) 29 | (.writeHead res (:status response) (->js (:headers response))) 30 | (.end res (:body response))) 31 | (fn [error] 32 | (let [msg 33 | (str "Error in request handler: " (.-message error) 34 | "\n\n" 35 | (.-stack error))] 36 | (println msg) 37 | (.writeHead res 500 #js {"Content-Type" "text/plain"}) 38 | (.end res msg))))))) 39 | LifeCycle 40 | (start! [server] 41 | (.listen server (:port opts) (:host opts "localhost"))) 42 | (stop! [server] 43 | (.close server)))) 44 | 45 | (comment 46 | (defn handler [req] 47 | {:status 200 48 | :content-type :json 49 | :body {:foo "bar"}}) 50 | 51 | (defn json-body-mw [handler] 52 | (fn ^:async h [req] 53 | (let [res (await (handler req))] 54 | (if (= :json (:content-type res)) 55 | (-> res 56 | (assoc-in [:headers "Content-Type"] "application/json") 57 | (update :body (comp js:JSON.stringify ->js))) 58 | res)))) 59 | 60 | (def server (-> handler 61 | json-body-mw 62 | (create-server {:port 1234}))) 63 | 64 | (start! server)) 65 | -------------------------------------------------------------------------------- /packages/piglet/src/node/pig-cli.pig: -------------------------------------------------------------------------------- 1 | (module node/pig-cli 2 | "Implementation of the `pig` project management tool 3 | 4 | This is one of the two main entry points for piglet, the other being the 5 | `piglet` interpreter" 6 | (:import 7 | [parseargs :from cli/parseargs] 8 | [term :from cli/terminal] 9 | [fs :from "node:fs"] 10 | [NodeREPL :from "../../../../lib/piglet/node/NodeREPL.mjs"])) 11 | 12 | (defn ^:async maybe-load-current-package [] 13 | (when (fs:existsSync "package.pig") 14 | (await (load-package ".")))) 15 | 16 | (defn repl 17 | {:doc "Start a Piglet REPL" 18 | :async true} 19 | [opts] 20 | (when (:verbose opts) 21 | (set! *verbosity* (:verbose opts))) 22 | ;; Wait a tick so this module has finished loading, so we don't briefly show 23 | ;; a prompt with a current-module of node/pig-cli 24 | (js:setTimeout 25 | (fn ^:async [] 26 | (await (maybe-load-current-package)) 27 | (set! *current-module* (ensure-module (fqn *current-package*) "user")) 28 | (.start_readline (NodeREPL. *compiler* #js {}))) 29 | 0)) 30 | 31 | (defn 32 | pdp 33 | {:doc "Connect to Piglet Dev Protocol server (i.e. your editor)" 34 | :async true 35 | :flags ["--host,-h=" {:doc "Hostname or IP address to connect to" 36 | :default "127.0.0.1"} 37 | "--port, -p=" {:doc "Port to connect on" 38 | :default 17017} 39 | "--[no]-ssl" {:doc "Use SSL (wss protocol instead of ws)"}]} 40 | [opts] 41 | (await (require 'pdp-client)) 42 | (await (maybe-load-current-package)) 43 | (let [url (str (if (:ssl opts) "wss" "ws") "://" (:host opts) ":" (:port opts))] 44 | (println (term:fg :cyan "Connecting to PDP on") (term:fg :yellow url)) 45 | ((resolve 'piglet:pdp-client:connect!) url))) 46 | 47 | (defn web 48 | {:doc "Start dev-server for web-based projects" 49 | :async true 50 | :flags ["--port, -p=" {:doc "Port to start http server on" 51 | :default 1234}]} 52 | [opts] 53 | (await (require 'node/dev-server)) 54 | ((resolve 'piglet:node/dev-server:main) opts)) 55 | 56 | (defn aot 57 | {:doc "AOT compile the given module and its dependencies" 58 | :async true} 59 | [{:keys [module] :as opts}] 60 | (await (maybe-load-current-package)) 61 | (await (compile (read-string module))) 62 | (await (compile 'piglet:lang)) 63 | (println "Copying runtime") 64 | (await (fs:cpSync (js:URL. "lib/piglet/lang" piglet-base-url) "target/piglet-lang.org/packages/lang" #js {:recursive true}))) 65 | 66 | (defn run 67 | ^:async 68 | [{:keys [module] :as opts}] 69 | (await (maybe-load-current-package)) 70 | (await (require (read-string module))) 71 | ) 72 | 73 | (def commands 74 | ["repl" #'repl 75 | "run " #'run 76 | "pdp" #'pdp 77 | "web" #'web 78 | "aot " #'aot]) 79 | 80 | (def flags 81 | ["--verbose, -v" "Increase verbosity"]) 82 | 83 | (parseargs:dispatch 84 | {:name "pig" 85 | :doc "Piglet's project tool" 86 | :commands commands 87 | :flags flags 88 | :middleware [(fn [cmd] 89 | (fn [opts] 90 | (when (:verbose opts) 91 | (set! *verbosity* (:verbose opts))) 92 | (cmd opts)))]} 93 | (drop 2 js:process.argv)) 94 | -------------------------------------------------------------------------------- /packages/piglet/src/pdp-client.pig: -------------------------------------------------------------------------------- 1 | (module pdp-client 2 | (:import 3 | piglet:cbor 4 | piglet:string)) 5 | 6 | ;; Walking skeleton for a Piglet Dev Protocol client 7 | ;; 8 | ;; Connect to ws://localhost:17017. Waits for CBOR-encoded messages with {"op" 9 | ;; "eval", "code" ...}, evaluates the code, and replies with {"op" "eval", 10 | ;; "result" result-str} 11 | 12 | (def WebSocket (if (undefined? js:WebSocket) 13 | @(.resolve (await (js-import "ws")) "default") 14 | js:WebSocket)) 15 | 16 | (defn completion-candidates [mod prefix] 17 | (filter (fn [n] 18 | (.startsWith n prefix)) 19 | (map (fn [v] (.-name v)) 20 | (vals mod)))) 21 | 22 | (defn on-message-fn [conn] 23 | (fn ^:async on-message [msg] 24 | (let [msg (cbor:decode (.-data msg)) 25 | _ (println '<- msg) 26 | {:keys [op code location module package var reply-to]} msg 27 | reply (fn [answer] 28 | (let [reply (cond-> {:op op} 29 | reply-to 30 | (assoc :to reply-to) 31 | :-> 32 | (into answer))] 33 | (println '-> reply) 34 | (.send conn (cbor:encode reply))))] 35 | (when (string? location) 36 | (.set_value (resolve '*current-location*) location)) 37 | (when (string? module) 38 | (.set_value (resolve '*current-module*) (.ensure_module module-registry package module))) 39 | (when (string? package) 40 | (.set_value (resolve '*current-package*) (.ensure_package module-registry package))) 41 | (cond 42 | (= "resolve-meta" op) 43 | (do 44 | (println "Resolving" var "in" *current-module* ":" (resolve (symbol var)) " / " (meta (resolve (symbol var)))) 45 | (let [var (resolve (symbol var)) 46 | val @var] 47 | (reply {:result (print-str (meta (if (instance? Module val) 48 | val 49 | var)))}))) 50 | 51 | (= "eval" op) 52 | (do 53 | (println code) 54 | (.then 55 | (.eval_string *compiler* code location (:start msg) (:line msg)) 56 | (fn [val] 57 | (println '=> val) 58 | (reply {:result (print-str val)})) 59 | (fn [err] 60 | (js:console.log err) 61 | (reply {:result (print-str err)})))) 62 | 63 | (= "completion-candidates" op) 64 | (let [prefix (:prefix msg)] 65 | (cond 66 | ;; TODO (.includes prefix "://") 67 | (and (string? prefix) (string:includes? prefix ":")) 68 | (let [[alias suffix] (string:split ":" prefix)] 69 | (if-let [mod (find-module (symbol alias))] 70 | (reply {:candidates (map (fn [c] (str alias ":" c)) 71 | (completion-candidates mod suffix))}) 72 | (reply {:candidates []}))) 73 | :else 74 | (reply {:candidates (concat 75 | (completion-candidates (find-module 'piglet:lang) prefix) 76 | (when (not= (find-module 'piglet:lang) *current-module*) 77 | (completion-candidates *current-module* prefix)))}))))))) 78 | 79 | (defn connect! [uri] 80 | (let [conn (WebSocket. uri)] 81 | (set! (.-onerror conn) (fn [{:keys [error]}] 82 | (let [{:keys [code address port]} error] 83 | (when (= "ECONNREFUSED" code) 84 | (println "ERROR: Connection to PDP server at" (str "ws://" address ":" port) "failed. Is the server running?") 85 | (when (not (undefined? js:process)) 86 | (js:process.exit -1)))))) 87 | 88 | (set! (.-onmessage conn) (on-message-fn conn)) 89 | 90 | (set! (.-binaryType conn) "arraybuffer"))) 91 | -------------------------------------------------------------------------------- /packages/piglet/src/reactive.pig: -------------------------------------------------------------------------------- 1 | (module reactive 2 | "Reactive primitives for UI programming") 3 | 4 | (def ^:dynamic *reactive-context* 5 | "Bind to a JS array to track `deref`s of reactive cells. Any cell that is 6 | derefed will push itself onto the array, and can subsequently be watched. " 7 | nil) 8 | 9 | (defn cell 10 | "Reactive container. Like [[box]], but can participate in a reactive signal 11 | graph. See also [[formula]]. Takes the initial value as argument." 12 | [v] 13 | (specify! #js {:val v 14 | :watches {} 15 | :notify #{}} 16 | Swappable 17 | (-swap! [this f args] 18 | (let [old-val (.-val this) 19 | new-val (apply f old-val args)] 20 | (set! (.-val this) new-val) 21 | (doseq [[k f] (.-watches this)] 22 | (f k this old-val new-val)) 23 | new-val)) 24 | 25 | Derefable 26 | (deref [this] 27 | (when *reactive-context* 28 | (.push *reactive-context* this)) 29 | (.-val this)) 30 | 31 | TaggedValue 32 | (-tag [this] "cell") 33 | (-tag-value [this] (.-val this)) 34 | 35 | Watchable 36 | (add-watch! [this key watch-fn] 37 | (set! (.-watches this) (assoc (.-watches this) key watch-fn)) 38 | this) 39 | (remove-watch! [this key] 40 | (set! (.-watches this) (dissoc (.-watches this) key)) 41 | this))) 42 | 43 | (defprotocol Formula 44 | (-recompute! [this])) 45 | 46 | (defn track-reactions! [this compute! update!] 47 | (binding [#'*reactive-context* #js []] 48 | (let [new-val (compute!) 49 | old-inputs (.-inputs this) 50 | new-inputs *reactive-context*] 51 | (set! (.-inputs this) new-inputs) 52 | (doseq [input (remove (set old-inputs) new-inputs)] 53 | (add-watch! input this (fn [k r o n] (update! o n)))) 54 | (doseq [input (remove (set new-inputs) old-inputs)] 55 | (remove-watch! input this)) 56 | new-val))) 57 | 58 | (defn cursor [cell path] 59 | (let [this #js {:watches {}}] 60 | (add-watch! cell this 61 | (fn [k r o n] 62 | (let [old (get-in o path) 63 | new (get-in n path)] 64 | (when (not= o n) 65 | (doseq [[k f] (.-watches this)] 66 | (f k this old new)))))) 67 | 68 | (specify! this 69 | Swappable 70 | (-swap! [this f args] 71 | (apply swap! cell update-in path f args) 72 | (get-in @cell path)) 73 | 74 | Derefable 75 | (deref [this] 76 | (when *reactive-context* 77 | (.push *reactive-context* this)) 78 | (binding [#'*reactive-context* nil] 79 | (get-in (lang:deref cell) path))) 80 | 81 | TaggedValue 82 | (-tag [this] "cursor") 83 | (-tag-value [this] {:cell cell :path path}) 84 | 85 | Watchable 86 | (add-watch! [this key watch-fn] 87 | (set! (.-watches this) (assoc (.-watches this) key watch-fn)) 88 | this) 89 | (remove-watch! [this key] 90 | (set! (.-watches this) (dissoc (.-watches this) key)) 91 | this)))) 92 | 93 | (defn formula* [thunk] 94 | (specify! #js {:val ::new 95 | :watches {} 96 | :inputs #js []} 97 | Swappable 98 | (-swap! [this f args] 99 | (let [old-val (.-val this) 100 | new-val (apply f old-val args)] 101 | (set! (.-val this) new-val) 102 | (doseq [[k f] (.-watches this)] 103 | (f k this old-val new-val)) 104 | new-val)) 105 | 106 | Derefable 107 | (deref [this] 108 | (when (= ::new (.-val this)) 109 | (-recompute! this)) 110 | (when *reactive-context* 111 | (.push *reactive-context* this)) 112 | (.-val this)) 113 | 114 | TaggedValue 115 | (-tag [this] "formula") 116 | (-tag-value [this] (.-val this)) 117 | 118 | Watchable 119 | (add-watch! [this key watch-fn] 120 | (set! (.-watches this) (assoc (.-watches this) key watch-fn)) 121 | this) 122 | (remove-watch! [this key] 123 | (set! (.-watches this) (dissoc (.-watches this) key)) 124 | this) 125 | 126 | Formula 127 | (-recompute! [this] 128 | (track-reactions! 129 | this 130 | (fn [] 131 | (reset! this (thunk))) 132 | (fn [old new] 133 | (when (not= old new) 134 | (-recompute! this))))))) 135 | 136 | (defmacro formula 137 | "Create a formula cell, containing a computed value based on the value of 138 | other cells (formula or regular), which will update automatically when any of 139 | the dependent cells update. Macro version, see [[formula*]] for a version 140 | which takes a zero-arity function (thunk)" 141 | [& body] 142 | `(formula* (fn [] ~@body))) 143 | -------------------------------------------------------------------------------- /packages/piglet/src/spec/all.pig: -------------------------------------------------------------------------------- 1 | (module spec/all 2 | (:import 3 | [u :from spec/util] 4 | [_ :from spec/destructuring] 5 | [_ :from spec/binding-forms]) 6 | (:context {"my-prefix" "https://arnebrasseur.net/vocab/"})) 7 | 8 | ;; Primitives 9 | 10 | (u:testing "Numbers" 11 | (u:testing 12 | "basic syntax" 13 | (u:is (= (type (read-string "123")) "number")) 14 | (u:is (= (read-string "123") 123)) 15 | (u:is (= (read-string "123.45") 123.45)))) 16 | 17 | (u:testing "Strings" 18 | (u:testing 19 | "basic syntax" 20 | (u:is (= "string" (type (read-string "\"abc\"")))) 21 | (u:is (= "abc" (read-string "\"abc\""))) 22 | (u:is (= %q(abc) "abc")) 23 | "escape sequences" 24 | (u:is (= 10 (.charCodeAt "\n" 0))) 25 | (u:is (= 9 (.charCodeAt "\t" 0))) 26 | (u:is (= "✊" "\u270a")) 27 | (u:is (= "✊" "\u270A")) 28 | (u:is (= "🨅" "\u{1fa05}")) 29 | "Seqable" 30 | (u:is (= ["a" "b" "c"] (seq "abc"))))) 31 | 32 | (u:testing "Regexp" 33 | (u:testing 34 | "basic syntax" 35 | (u:is (= %r"^a.*z$"m (js:RegExp. "^a.*z$" "m"))) 36 | "escaping" 37 | (u:is (= %r"a\"b" (js:RegExp. "a\"b"))) 38 | (u:is (= %r/a\/b/ (js:RegExp. "a/b"))) 39 | "freespacing (x)" 40 | (u:is (= %r/ 41 | This\ regex # 42 | /x 43 | (js:RegExp. "This regex"))) 44 | "stringify" 45 | (u:is (= "%r/x/g" (str %r/x/g))))) 46 | 47 | (u:testing "QName" 48 | (u:testing 49 | "Basic constructions" 50 | (u:is (= :https://vocabe.piglet-lang.org/package/name (qname "https://vocabe.piglet-lang.org/package/name"))) 51 | 52 | "Built-in context" 53 | (u:is (= :foaf:name :http://xmlns.com/foaf/0.1/name)) 54 | 55 | "Module context" 56 | (u:is (= :my-prefix:fruit :https://arnebrasseur.net/vocab/fruit)) 57 | 58 | "Self reference" 59 | (u:is (= ::fruit :https://piglet-lang.org/packages/piglet#fruit)) 60 | )) 61 | 62 | ;; Collections 63 | 64 | (u:testing "Dict" 65 | (u:testing 66 | "implements DictLike" 67 | ;; TODO: this won't always return things in order once we have proper 68 | ;; persistent dicts 69 | (u:is (= [:a :b :c] (keys {:a 1 :b 2 :c 3}))) 70 | (u:is (= [1 2 3] (vals {:a 1 :b 2 :c 3})))) 71 | 72 | (u:testing 73 | "Basic constructions" 74 | (let [m0 {} 75 | m1 {:foo "bar"} 76 | m2 {:foo "bar" :bar "baz"} 77 | m-str-key {"foo" "bar"} 78 | m-int-key {123 "world"}] 79 | (u:is (= m0 {})) 80 | (u:is (= m0 (dict))) 81 | (u:is (= m1 (.of Dict nil :foo "bar"))) 82 | (u:is (= m1 (dict :foo "bar"))) 83 | (u:is (= m-str-key (assoc {} "foo" "bar"))) 84 | (u:is (= m-int-key {123 "world"})))) 85 | 86 | (u:testing 87 | "Retrieving elements" 88 | (u:is (= (:foo {:foo "bar"}) "bar")))) 89 | 90 | (u:testing "HashSet" 91 | (u:testing 92 | "basic syntax" 93 | (u:is (= #{1 2 3} (HashSet.of nil 1 2 3))) 94 | (u:is (= #{1 2 3 4} #{4 3 2 1 2 3 4})) 95 | (u:is (= #{1 2 3} (set [1 2 3]))) 96 | (u:is (= #{1 2 3} #{(inc 0) (+ 1 1) (/ 6 2)})) 97 | "use as a function" 98 | (u:is (= 1 (#{1 2 3} 1))) 99 | (u:is (= nil (#{1 2 3} 4))) 100 | "conj / dissoc" 101 | (u:is (= #{1 2 3 4} (conj #{1 2 3} 4))) 102 | (u:is (= #{1 2 3} (conj #{1 2 3} 3))) 103 | (u:is (= #{1 2} (dissoc #{1 2 3} 3))) 104 | (u:is (= #{1 2 3} (dissoc #{1 2 3} 4))) 105 | "predicate" 106 | (u:is (set? #{})) 107 | (u:is (set? #{1 2})) 108 | (u:is (not (set? nil))) 109 | (u:is (not (set? (js:Set.)))) 110 | "Counted" 111 | (u:is (= 2 (count #{1 2}))))) 112 | 113 | (u:testing "Dynamic bindings" 114 | (u:is (= 3 (binding [*verbosity* 3] *verbosity*)))) 115 | 116 | (defn recursive-function [n] 117 | (if (< 5 n) n (recur (inc n)))) 118 | 119 | (u:testing 120 | "Loop/recur" 121 | (u:testing 122 | "Function as loop-head" 123 | (u:is (= 6 (recursive-function 0))) 124 | "loop/recur" 125 | (u:is (= 6 (loop [n 0] (if (< 5 n) n (recur (inc n)))))))) 126 | 127 | ;; Regressions 128 | 129 | (u:testing 130 | "Map works on iterator (no double consumption)" 131 | (u:is (= [["x"] ["x"] ["x"]] 132 | (map identity 133 | (.matchAll "xxx" %r/x/g))))) 134 | -------------------------------------------------------------------------------- /packages/piglet/src/spec/binding-forms.pig: -------------------------------------------------------------------------------- 1 | (module spec/binding-forms 2 | (:import [u :from spec/util])) 3 | 4 | (u:testing 5 | "let blocks" 6 | (u:is (= [1 2 3] 7 | (let [a 1 b 2 c 3] 8 | [a b c]))) 9 | "nested lets" 10 | (u:is (= [4 2 3 5] 11 | (let [a 1 b 2 c 3] 12 | (let [a 4 d 5] 13 | [a b c d]))))) 14 | -------------------------------------------------------------------------------- /packages/piglet/src/spec/util.pig: -------------------------------------------------------------------------------- 1 | (module spec/util) 2 | 3 | (def indent 0) 4 | 5 | (defn msg [& args] 6 | (println (str (apply str (repeat indent " ")) 7 | (.join (js:Array.from args) " ")))) 8 | 9 | (defmacro is [form] 10 | (let [[pred] form] 11 | `(if ~form 12 | (msg "\u001b[32m[OK]\u001b[0m" ~(print-str form)) 13 | ~(cond 14 | (= '= pred) 15 | `(msg "\u001b[31m[!!]" ~form "\u001b[0m" 16 | "Expected" ~(print-str (nth form 2)) 17 | "to be" ~(print-str (nth form 1)) 18 | ", got" (print-str ~(nth form 2))))))) 19 | 20 | (defmacro testing [& body] 21 | (cons 'do 22 | (reduce 23 | (fn [acc form] 24 | (conj acc 25 | (if (string? form) 26 | `(msg ~form) 27 | `(do 28 | (set! indent (+ indent 2)) 29 | ~form 30 | (set! indent (- indent 2)))))) 31 | `[] 32 | body))) 33 | -------------------------------------------------------------------------------- /packages/piglet/src/string.pig: -------------------------------------------------------------------------------- 1 | (module string) 2 | 3 | (defn upcase [s] 4 | (when s 5 | (.toUpperCase (str s)))) 6 | 7 | (defn downcase [s] 8 | (when s 9 | (.toLowerCase (str s)))) 10 | 11 | (defn subs 12 | ([s start] 13 | (when s 14 | (.slice (str s) start))) 15 | ([s start end] 16 | (when s 17 | (.slice (str s) start end)))) 18 | 19 | (defn capitalize [s] 20 | (when s 21 | (let [s (str s)] 22 | (str (upcase (first s)) (downcase (subs s 1)))))) 23 | 24 | (defn replace [s match replace] 25 | (when s 26 | (.replaceAll s (if (some #{"g"} (.-flags match)) 27 | match 28 | (js:RegExp. match (str (.-flags match) "g"))) 29 | replace))) 30 | 31 | (defn starts-with? [s prefix] 32 | (when s 33 | (.startsWith s prefix))) 34 | 35 | (defn ends-with? [s prefix] 36 | (when s 37 | (.endsWith s prefix))) 38 | 39 | (defn includes? [s substring] 40 | (when s 41 | (.includes s substring))) 42 | 43 | (defn trim [s] 44 | (when s 45 | (.trim s))) 46 | 47 | ;; separator first, for partial application 48 | (defn join 49 | ([strings] 50 | (apply str strings)) 51 | ([sep strings] 52 | (.join (js:Array.from strings 53 | ;; Prevent the idx argument being passed to str 54 | (fn [s] (str s))) sep))) 55 | 56 | (defn split [sep string] 57 | (when string 58 | (.split string sep))) 59 | 60 | (def split-kebab (partial split "-")) 61 | (def split-snake (partial split "_")) 62 | (def split-camel (comp 63 | (partial map downcase) 64 | (partial split %r/(?=[A-Z])/))) 65 | 66 | (def join-kebab (partial join "-")) 67 | (def join-snake (partial join "_")) 68 | (def join-camel (comp 69 | (partial join "") 70 | (partial map capitalize))) 71 | 72 | (defn join-dromedary [parts] 73 | (apply str 74 | (first parts) 75 | (map capitalize (rest parts)))) 76 | 77 | (def kebab->snake (comp join-snake split-kebab)) 78 | (def kebab->camel (comp join-camel split-kebab)) 79 | (def kebab->dromedary (comp join-dromedary split-kebab)) 80 | 81 | (def snake->kebab (comp join-kebab split-snake)) 82 | (def snake->camel (comp join-camel split-snake)) 83 | (def snake->dromedary (comp join-dromedary split-snake)) 84 | 85 | (def camel->kebab (comp join-kebab split-camel)) 86 | (def camel->snake (comp join-snake split-camel)) 87 | (defn camel->dromedary [s] 88 | (str (downcase (first s)) (subs s 1))) 89 | 90 | (def dromedary->snake camel->snake) 91 | (def dromedary->kebab camel->kebab) 92 | (defn dromedary->camel [s] 93 | (str (upcase (first s)) (subs s 1))) 94 | 95 | (def pad-start 96 | (if (fn? (.-padStart "")) ;; possibly polyfill 97 | (fn 98 | ([s pad] 99 | (.padStart s pad)) 100 | ([s pad ch] 101 | (.padStart s pad ch))) 102 | (fn 103 | ([s pad] 104 | (pad-start s pad " ")) 105 | ([s pad ch] 106 | (str 107 | (apply str (repeat (- pad (count s)) ch)) 108 | s))))) 109 | 110 | (def pad-end 111 | (if (fn? (.-padEnd "")) ;; possibly polyfill 112 | (fn 113 | ([s pad] 114 | (.padEnd s pad)) 115 | ([s pad ch] 116 | (.padEnd s pad ch))) 117 | (fn 118 | ([s pad] 119 | (pad-end s pad " ")) 120 | ([s pad ch] 121 | (str 122 | s 123 | (apply str (repeat (- pad (count s)) ch))))))) 124 | 125 | (defn blank? [s] 126 | (or 127 | (nil? s) 128 | (= "" s) 129 | (boolean (re-find #"^\W+$" s)))) 130 | 131 | (comment 132 | (snake->kebab 133 | (camel->snake 134 | (dromedary->camel 135 | (kebab->dromedary "xxx-yyy-zzz"))))) 136 | -------------------------------------------------------------------------------- /packages/piglet/src/test.pig: -------------------------------------------------------------------------------- 1 | (module test 2 | (:import 3 | [s :from piglet:string])) 4 | 5 | (println (s:replace "xxx" %r"x" "y")) 6 | (println #'expand-qnames) 7 | -------------------------------------------------------------------------------- /packages/piglet/src/web/ui.pig: -------------------------------------------------------------------------------- 1 | (module web/ui 2 | (:import 3 | [i :from "./inferno.mjs"] 4 | string dom reactive)) 5 | 6 | (def literal? #{"string" "boolean" "number" "bigint" 7 | js:Date js:RegExp Sym Keyword PrefixName QName QSym}) 8 | 9 | (def kebab-case-tags 10 | #{"accept-charset" "http-equiv" "accent-height" 11 | "alignment-baseline" "arabic-form" "baseline-shift" "cap-height" "clip-path" 12 | "clip-rule" "color-interpolation" "color-interpolation-filters" "color-profile" 13 | "color-rendering" "fill-opacity" "fill-rule" "flood-color" "flood-opacity" 14 | "font-family" "font-size" "font-size-adjust" "font-stretch" "font-style" 15 | "font-variant" "font-weight" "glyph-name" "glyph-orientation-horizontal" 16 | "glyph-orientation-vertical" "horiz-adv-x" "horiz-origin-x" "marker-end" 17 | "marker-mid" "marker-start" "overline-position" "overline-thickness" "panose-1" 18 | "paint-order" "stop-color" "stop-opacity" "strikethrough-position" 19 | "strikethrough-thickness" "stroke-dasharray" "stroke-dashoffset" 20 | "stroke-linecap" "stroke-linejoin" "stroke-miterlimit" "stroke-opacity" 21 | "stroke-width" "text-anchor" "text-decoration" "text-rendering" 22 | "underline-position" "underline-thickness" "unicode-bidi" "unicode-range" 23 | "units-per-em" "v-alphabetic" "v-hanging" "v-ideographic" "v-mathematical" 24 | "vert-adv-y" "vert-origin-x" "vert-origin-y" "word-spacing" "writing-mode" 25 | "x-height"}) 26 | 27 | (def svg-tags 28 | #{"a" "animate" "animateMotion" "animateTransform" "circle" 29 | "clipPath" "defs" "desc" "ellipse" "feBlend" "feColorMatrix" 30 | "feComponentTransfer" "feComposite" "feConvolveMatrix" "feDiffuseLighting" 31 | "feDisplacementMap" "feDistantLight" "feDropShadow" "feFlood" "feFuncA" 32 | "feFuncB" "feFuncG" "feFuncR" "feGaussianBlur" "feImage" "feMerge" "feMergeNode" 33 | "feMorphology" "feOffset" "fePointLight" "feSpecularLighting" "feSpotLight" 34 | "feTile" "feTurbulence" "filter" "foreignObject" "g" "hatch" "hatchpath" "image" 35 | "line" "linearGradient" "marker" "mask" "metadata" "mpath" "path" "pattern" 36 | "polygon" "polyline" "radialGradient" "rect" "script" "set" "stop" "style" "svg" 37 | "switch" "symbol" "text" "textPath" "title" "tspan" "use" "view"}) 38 | 39 | (defn convert-attr-name [attr] 40 | (let [attr (name attr)] 41 | (if (or 42 | (kebab-case-tags attr) 43 | (string:starts-with? attr "data-") 44 | (string:starts-with? attr "aria-") 45 | (string:starts-with? attr "hx-")) 46 | attr 47 | (string:kebab->dromedary attr)))) 48 | 49 | (def ^:inline FLAG_HTML_ELEMENT 1) 50 | (def ^:inline FLAG_CLASS_COMPONENT 4) 51 | (def ^:inline FLAG_FUNCTION_COMPONENT 8) 52 | 53 | (declare h) 54 | 55 | (defn convert-function-component [c] 56 | (if-let [w (.-pigferno_wrapper_component c)] 57 | w 58 | (fn ctor [props context] 59 | (set! (.-pigferno_wrapper_component c) ctor) 60 | (let [w (i:Component. props context)] 61 | (set! (.-render w) 62 | (fn [props] 63 | (reactive:track-reactions! 64 | w 65 | (fn [] 66 | (let [props (.-pigferno_args props) 67 | res (apply c props)] 68 | (h (if (fn? res) 69 | (apply res props) 70 | res)))) 71 | (fn [old new] 72 | (when (not= old new) 73 | (.forceUpdate w)))))) 74 | 75 | w)))) 76 | 77 | (defn h 78 | "Convert a representation of HTML as a Piglet data structure into virtual DOM nodes. 79 | 80 | Strings and other literals (numbers, dates, etc.) converted to text nodes. 81 | Vectors are converted based on the first element. 82 | 83 | - Keyword: used as tag name for a (V)DOM element. Optionally followed by a 84 | dict of props, followed by child elements 85 | - Function: used as a component, anything that follows is used as arguments 86 | for the component 87 | " 88 | [o] 89 | (cond 90 | (string? o) 91 | (i:createTextVNode o) 92 | 93 | (literal? (type o)) 94 | (i:createTextVNode o) 95 | 96 | (vector? o) 97 | (let [[tag & children] o] 98 | (cond 99 | (= :<> tag) 100 | (i:createFragment (into-array (map h children)) 0) 101 | 102 | (keyword? tag) 103 | (let [[base tag id klasses] (dom:split-tag tag) 104 | [props & children*] children 105 | [props children] (if (dict? props) 106 | [props children*] 107 | [nil children])] 108 | (i:createVNode FLAG_HTML_ELEMENT 109 | tag 110 | (string:join " " (concat klasses (cond 111 | (sequential? (:class props)) 112 | (remove nil? (:class props)) 113 | (some? (:class props)) 114 | (str (:class props))))) 115 | (into-array (map h children)) 116 | 0 117 | (into (if id #js {:id id} #js {}) 118 | (map (juxt 119 | (comp convert-attr-name first) 120 | second) 121 | (dissoc props :class :ref))) 122 | (:key (meta o)) 123 | (:ref props))) 124 | 125 | (fn? tag) 126 | (i:createComponentVNode FLAG_CLASS_COMPONENT 127 | (convert-function-component tag) 128 | #js {:pigferno_args children} 129 | (:key (meta o)) 130 | nil))) 131 | 132 | (sequential? o) 133 | (i:createFragment (into-array (map h o)) 0) 134 | )) 135 | 136 | (defn render 137 | "Mount the Piglet html form `input` in the DOM element `parent-dom`." 138 | [parent-dom input] 139 | (i:render (h input) parent-dom)) 140 | -------------------------------------------------------------------------------- /repl_sessions/bindings.pig: -------------------------------------------------------------------------------- 1 | (module foo 2 | (:import [astring :from "astring"]) ) 3 | 4 | (do 5 | (def ana (.-analyzer *compiler*)) 6 | 7 | (set! ana.ff true) 8 | 9 | (map astring:generate 10 | (.emit 11 | (.analyze ana '(let [[x] [1]] (let [y 3] y) ) ) 12 | (.-code_gen *compiler*))) 13 | #_ 14 | (map astring:generate 15 | (.emit 16 | (.analyze ana 17 | '(loop [x 1] 18 | (when (< x 10) 19 | (recur (inc x))))) 20 | (.-code_gen *compiler*)))) 21 | 22 | (let [cmdspec :cmdspec 23 | cli-args ["a" "b" "c"] 24 | opts {:x 1}] 25 | (loop [cmdspec cmdspec 26 | [arg & cli-args] cli-args 27 | args [] 28 | seen-prefixes #{} 29 | opts opts] 30 | ;; Handle additional flags by nested commands 31 | (let [opts (assoc opts :y 2) 32 | cmdspec [:cmdspec cmdspec]] 33 | #_{:cmdspec cmdspec 34 | :arg arg 35 | :cli-args cli-args 36 | :args args 37 | :seen-prefixes seen-prefixes 38 | :opts opts} 39 | (when (seq cli-args) 40 | (recur 41 | cmdspec 42 | cli-args 43 | args 44 | seen-prefixes 45 | opts))))) 46 | -------------------------------------------------------------------------------- /repl_sessions/cli-test.pig: -------------------------------------------------------------------------------- 1 | (module cli-test 2 | (:import 3 | [cli :from piglet:cli/port] 4 | [str :from piglet:string])) 5 | 6 | (defn list-cmd 7 | "List things" 8 | [opts] 9 | (prn opts)) 10 | 11 | (def commands 12 | ["list" #'list-cmd]) 13 | 14 | (def flags 15 | ["--flag ARG" "Do a thing"]) 16 | 17 | (cli:dispatch 18 | {:name "cli-test" 19 | :commands commands 20 | :flags flags} 21 | (drop 3 js:process.argv) 22 | #_ ["list"]) 23 | 24 | ;; (defn x 25 | ;; ([]) 26 | ;; ([a] (loop [a a]))) 27 | 28 | ;; (fn [] 29 | ;; (fn [] 30 | ;; (recur))) 31 | 32 | 33 | ;; (fn [] 34 | ;; (let [reply 35 | ;; (fn [answer] 36 | ;; (let [x 1] 37 | ;; (println 1) 38 | ;; (println 1) 39 | ;; ))] 40 | ;; )) 41 | -------------------------------------------------------------------------------- /repl_sessions/comments.mjs: -------------------------------------------------------------------------------- 1 | // Make sure acorn, astravel and astring modules are imported 2 | 3 | import * as acorn from "acorn" 4 | import * as astring from "astring" 5 | import * as astravel from "astravel" 6 | 7 | // Set example code 8 | var code = 9 | [ 10 | "const foo = /* @__PURE__ */((e)=>e)" 11 | ].join('\n') + '\n' 12 | // Parse it into an AST and retrieve the list of comments 13 | var comments = [] 14 | var ast = acorn.parse(code, { 15 | ecmaVersion: 6, 16 | locations: true, 17 | onComment: comments, 18 | }) 19 | // Attach comments to AST nodes 20 | astravel.attachComments(ast, comments) 21 | 22 | console.dir(ast, {depth: null}) 23 | 24 | // Format it into a code string 25 | var formattedCode = astring.generate(ast, { 26 | comments: true, 27 | }) 28 | // Check it 29 | // console.log(code === formattedCode ? 'It works!' : 'Something went wrong…') 30 | -------------------------------------------------------------------------------- /repl_sessions/comments.pig: -------------------------------------------------------------------------------- 1 | (module try-comments 2 | ) 3 | -------------------------------------------------------------------------------- /repl_sessions/dom_stuff.pig: -------------------------------------------------------------------------------- 1 | 2 | 3 | (def tmpl 4 | (.createElement js:document "template")) 5 | 6 | (set! (.-innerHTML tmpl) "
hello
") 7 | 8 | (.cloneNode tmpl) 9 | -------------------------------------------------------------------------------- /repl_sessions/package_stuff.pig: -------------------------------------------------------------------------------- 1 | (println "script mode!") 2 | 3 | (.inspect (quote https://foo.bar/baz)) 4 | 5 | (oget (qsym "https://foo.bar/baz:bar:baq") "var") 6 | -------------------------------------------------------------------------------- /repl_sessions/print_parse_tree.mjs: -------------------------------------------------------------------------------- 1 | import * as astring from 'astring' 2 | import {Parser} from 'acorn' 3 | 4 | const path = 'node:' + 'process' 5 | const process = await import(path) 6 | 7 | 8 | // console.log( 9 | // Parser.extend(classFields).parse('class X { x = 0 }', {ecmaVersion: 2020}) 10 | // ) 11 | 12 | function print_parse_tree(... code) { 13 | for (let c of code) { 14 | console.log(c) 15 | console.dir(Parser.parse(c, {allowAwaitOutsideFunction: true, ecmaVersion: 2022}).body[0], {depth: null}) 16 | console.dir(astring.generate(Parser.parse(c, {allowAwaitOutsideFunction: true, ecmaVersion: 2022}).body[0], {depth: null})) 17 | } 18 | } 19 | 20 | print_parse_tree( 21 | // '{const x = foo(); const y = bar(x); do_thing(x,y)}', 22 | // '{const [x,y] = foo()}', 23 | // '{const {x,y} = foo()}', 24 | // '{const {x: x_,y: y_} = foo()}', 25 | // '{const {x: x_ = 123,y: y_} = foo()}', 26 | // "const [x,y] = 1" 27 | // 'async function foo() { await impor("bar")}' 28 | // '( function() { let x = {(123);}; return x })' 29 | // 'throw new Error("x")', 30 | // 'typeof x', 31 | // 'try {} catch (e) {} finally {}' 32 | // "aoo(await(bar.baz()))" 33 | "class xxx { foo = 123 }" 34 | ) 35 | -------------------------------------------------------------------------------- /repl_sessions/test.js: -------------------------------------------------------------------------------- 1 | class Callable { 2 | constructor(name) { 3 | const self = {[name](...args) { return self.invoke(...args) }}[name] 4 | Object.setPrototypeOf(self, Callable.prototype) 5 | return self 6 | } 7 | 8 | invoke(a,b,c) { 9 | return a + b + c 10 | } 11 | } 12 | Object.setPrototypeOf(Callable.prototype, Function) 13 | 14 | class Obj { 15 | invoke(a,b,c) { 16 | return a + b + c 17 | } 18 | } 19 | 20 | function simple_function(a,b,c) { 21 | return a + b + c 22 | } 23 | 24 | const obj = new Obj() 25 | const callable = new Callable("my-fn") 26 | //warmup 27 | console.log([...Array(10000).keys()].reduce((acc,x) => obj.invoke(acc,x,x))) 28 | console.log([...Array(10000).keys()].reduce((acc,x) => callable(acc,x,x))) 29 | console.log([...Array(10000).keys()].reduce((acc,x) => simple_function(acc,x,x))) 30 | console.log([...Array(10000).keys()].reduce((acc,x) => obj.invoke(acc,x,x))) 31 | console.log([...Array(10000).keys()].reduce((acc,x) => callable(acc,x,x))) 32 | console.log([...Array(10000).keys()].reduce((acc,x) => simple_function(acc,x,x))) 33 | 34 | console.time("object") 35 | console.log([...Array(100000).keys()].reduce((acc,x) => obj.invoke(acc,x,x))) 36 | console.timeEnd("object") 37 | 38 | console.time("callable") 39 | console.log([...Array(100000).keys()].reduce((acc,x) => callable(acc,x,x))) 40 | console.timeEnd("callable") 41 | 42 | console.time("simple_function") 43 | console.log([...Array(100000).keys()].reduce((acc,x) => simple_function(acc,x,x))) 44 | console.timeEnd("simple_function") 45 | 46 | console.time("object") 47 | console.log([...Array(100000).keys()].reduce((acc,x) => obj.invoke(acc,x,x))) 48 | console.timeEnd("object") 49 | 50 | console.time("callable") 51 | console.log([...Array(100000).keys()].reduce((acc,x) => callable(acc,x,x))) 52 | console.timeEnd("callable") 53 | 54 | console.time("simple_function") 55 | console.log([...Array(100000).keys()].reduce((acc,x) => simple_function(acc,x,x))) 56 | console.timeEnd("simple_function") 57 | -------------------------------------------------------------------------------- /repl_sessions/typed_array_copy.pig: -------------------------------------------------------------------------------- 1 | (module typed-array-copy) 2 | 3 | (def types [ 4 | js:Int8Array 5 | js:Uint8Array 6 | js:Int16Array 7 | js:Uint16Array 8 | js:Int32Array 9 | js:Uint32Array 10 | js:Float32Array 11 | js:Float64Array 12 | js:BigInt64Array 13 | js:BigUint64Array 14 | ]) 15 | 16 | 17 | (def data (js:ArrayBuffer. 4096)) 18 | (def target (js:ArrayBuffer. 4096)) 19 | 20 | (def dv (js:DataView. data)) 21 | 22 | (doseq [i (range 4096)] 23 | (.setUint8 dv i (rand-int 256))) 24 | 25 | (doseq [t (reverse types)] 26 | (println t) 27 | (time 28 | (doseq [i (range 10000000)] 29 | (.set (t. target) (t. data))))) 30 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve' 2 | import replace from '@rollup/plugin-replace' 3 | import terser from '@rollup/plugin-terser' 4 | 5 | import piglet from './rollup_piglet.mjs' 6 | 7 | const browser = {output: {}, plugins: []} 8 | const node = {output: {}, plugins: []} 9 | const lang = {output: {}, plugins: []} 10 | 11 | browser.onwarn = node.onwarn = (e, warn) => (e.code === 'EVAL') ? "" : warn(e) 12 | 13 | browser.input = 'lib/piglet/browser/main.mjs' 14 | browser.output.file = 'dist/piglet.browser.mjs' 15 | browser.plugins.push(nodeResolve(), terser({module: true, ecma: 2018})) 16 | 17 | node.input = 'lib/piglet/node/main.mjs' 18 | node.output.file = 'dist/piglet.node.mjs' 19 | node.output.inlineDynamicImports = true 20 | node.plugins.push( 21 | nodeResolve(), 22 | replace({ 23 | values: { 24 | PIGLET_PACKAGE_PATH: 'path.join(createRequire(import.meta.url).resolve("piglet-lang"), "../../packages/piglet")' 25 | }, 26 | preventAssignment: true 27 | }), 28 | terser({module: true, ecma: 2018}) 29 | ) 30 | 31 | // lang.input = 'lib/piglet/lang.mjs' 32 | lang.input = 'packages/piglet/src/lang.pig' 33 | lang.output.file = 'dist/lang.mjs' 34 | lang.plugins.push(piglet(), terser({module: true, ecma: 2018})) 35 | 36 | export default [lang] //[browser, node, lang] 37 | -------------------------------------------------------------------------------- /rollup_piglet.mjs: -------------------------------------------------------------------------------- 1 | import {createFilter} from '@rollup/pluginutils' 2 | import * as path from 'node:path' 3 | import * as url from "node:url" 4 | import {intern, symbol, qsym, module_registry} from "./lib/piglet/lang.mjs" 5 | import {PIGLET_PKG} from "./lib/piglet/lang/util.mjs" 6 | 7 | import NodeCompiler from './lib/piglet/node/NodeCompiler.mjs' 8 | 9 | const compiler = new NodeCompiler() 10 | global.$piglet$ = module_registry.index 11 | 12 | const PIGLET_PACKAGE_PATH = path.join(url.fileURLToPath(import.meta.url), "../packages/piglet") 13 | await compiler.load_package(PIGLET_PACKAGE_PATH); 14 | intern(symbol('piglet:lang:*compiler*'), compiler) 15 | 16 | export default function(options) { 17 | return { 18 | transform: async function(code, id) { 19 | let out = "" 20 | compiler.hooks.js = (s) => out+=(s+";") 21 | await compiler.load(qsym(`${PIGLET_PKG}:lang`)) 22 | await compiler.load_file(id) 23 | return { 24 | code: out, 25 | map: {mappings: ''} 26 | }; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scratch.pig: -------------------------------------------------------------------------------- 1 | (+ 1 1) 2 | 3 | (def Protocol (.-default (await (js:import "../lib/piglet/lang/Protocol2.mjs")))) 4 | 5 | (def p (new Protocol nil "piglet:lang" "Eq", 6 | [["=" [[["this", "that"] "checks equality"]]]])) 7 | (js:console.log p) 8 | 9 | (js:console.dir 10 | (.-symreg Protocol)) 11 | 12 | (.intern p *current-module*) 13 | 14 | (.-name (first (js:Object.values (.-methods p)))) 15 | 16 | (reset-meta! (.resolve *current-module* "=") [:a]) 17 | 18 | (meta (.resolve *current-module* "=")) 19 | -------------------------------------------------------------------------------- /webdemo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /webdemo/solid-demo.pig: -------------------------------------------------------------------------------- 1 | (module solid-demo 2 | (:import 3 | [_ :from piglet:pdp-client] 4 | [dom :from piglet:dom] 5 | [solid :from "solid-js"] 6 | [solid-web :from "solid-js/web"] 7 | [p5 :from "p5"])) 8 | 9 | ;; FIXME 10 | ;; (.-Color 11 | ;; (do p5:default)) 12 | 13 | (.-Color p5:default) 14 | 15 | ;; also on window.p5 16 | (do js:p5.Color) 17 | 18 | ;; FIXME 19 | ;; (def {:props [Color Camera Matrix]} p5:default) 20 | 21 | (js:console.log (.-index module-registry)) 22 | 23 | (defn signal [init] 24 | (let [[getter setter] (solid:createSignal init)] 25 | (specify! setter 26 | Derefable 27 | (deref [_] (getter)) 28 | Swappable 29 | (-swap! [_ f args] 30 | (setter (apply f (getter) args)))) 31 | setter)) 32 | 33 | (defn memo [f] 34 | (let [m (solid:createMemo f nil #js {:equals =})] 35 | (specify! m 36 | Derefable 37 | (deref [_] (m))) 38 | m)) 39 | 40 | (defmacro reaction [& body] 41 | (list 'memo (cons 'fn (cons [] body)))) 42 | 43 | (defn template [str] 44 | (let [tmpl (dom:el "template")] 45 | (set! (.-innerHTML tmpl) str) 46 | (fn [] 47 | (.-firstChild (.-content (.cloneNode tmpl true)))))) 48 | 49 | (defn Counter [] 50 | (let [counter (signal 0) 51 | square (reaction (* @counter @counter)) 52 | tmpl (template "

Hello from Piglet! 🐷

53 |

Count:

54 |

Square:

55 |
")] 56 | (js:setInterval (fn [] (swap! counter inc)) 1000) 57 | (fn [] 58 | (let [el (tmpl)] 59 | (solid-web:insert (dom:query el "#count") (fn [] @counter)) 60 | (solid-web:insert (dom:query el "#square") (fn [] @square)) 61 | el)))) 62 | 63 | (solid-web:render 64 | (fn [] (solid:createComponent Counter #js {})) 65 | (dom:id->el "app")) 66 | -------------------------------------------------------------------------------- /webdemo/solid_test.js: -------------------------------------------------------------------------------- 1 | import { template as _$template } from "solid-js/web"; 2 | import { delegateEvents as _$delegateEvents } from "solid-js/web"; 3 | import { createComponent as _$createComponent } from "solid-js/web"; 4 | import { memo as _$memo } from "solid-js/web"; 5 | import { insert as _$insert } from "solid-js/web"; 6 | const _tmpl$ = /*#__PURE__*/_$template(`