From 34e45e702fb7ef3a396e23053639758ca717e5a8 Mon Sep 17 00:00:00 2001 From: jingus Date: Sun, 19 Apr 2026 04:03:00 -0500 Subject: [PATCH] init --- .envrc | 1 + .gitignore | 2 + Cargo.lock | 370 ++++++++++++++++++ Cargo.toml | 9 + README.md | 42 ++ examples/main.jkl | 17 + flake.lock | 77 ++++ flake.nix | 11 + .../packages/boilerplate/boilerplate.py | 245 ++++++++++++ nix-modules/packages/boilerplate/default.nix | 32 ++ nix-modules/parts.nix | 10 + nix-modules/shell.nix | 26 ++ src/main.rs | 6 + src/parser/ast.rs | 46 +++ src/parser/mod.rs | 31 ++ src/token.rs | 184 +++++++++ 16 files changed, 1109 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 examples/main.jkl create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix-modules/packages/boilerplate/boilerplate.py create mode 100644 nix-modules/packages/boilerplate/default.nix create mode 100644 nix-modules/parts.nix create mode 100644 nix-modules/shell.nix create mode 100644 src/main.rs create mode 100644 src/parser/ast.rs create mode 100644 src/parser/mod.rs create mode 100644 src/token.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e71e34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.direnv/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fd9f533 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,370 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chumsky" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba4a05c9ce83b07de31b31c874e87c069881ac4355db9e752e3a55c11ec75a6" +dependencies = [ + "hashbrown", + "regex-automata 0.3.9", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "jackal" +version = "0.1.0" +dependencies = [ + "chumsky", + "logos", + "winnow", +] + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata 0.4.14", + "regex-syntax 0.8.10", + "syn", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "stacker" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..94278e8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "jackal" +version = "0.1.0" +edition = "2024" + +[dependencies] +chumsky = "0.12.0" +logos = "0.16.1" +winnow = "1.0.1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fbb787 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +## Basic hello world + +```c +open std::io; + +// const { io } = import!("std"); +pub main := fn() -> () { + io.println("Hello, world"); + io.Writer + iterator.map(color::Color::to_bytes) +}; +``` + +## Structured data + +```c +Color ::= Color { red: u8, green: u8, blue: u8 }; +``` + +## Union types + +```c +Result ::= Ok (T) | Err (E); + +Option ::= Some (T) | None; + +MaybeColor ::= SomeColor { red: u8, green: u8, blue: u8 } | None; +``` + +# Symbol Semantics + +- `=` => Equal by assignment (or reassignment) +- `:=` => Equal by definition +- `::=` => Equal in structure or type +- `//` => in-line comment +- `///` => in-line doc comment +- `//!` => in-line module doc comment +- `|` => type union + +## Classes + +Same thing as a Haskell typeclass or Rust trait. diff --git a/examples/main.jkl b/examples/main.jkl new file mode 100644 index 0000000..6f0ba3c --- /dev/null +++ b/examples/main.jkl @@ -0,0 +1,17 @@ +//! Module-level documentation + +open std::io; + +Result ::= Ok (T) | Err (E); + +/// This just logs a thing +log := fn(data ::= Display) { + io::println("[info] {}", data); +}; + +// we need a main function +pub main := fn() -> () { + io::println("Hello, world"); + data := [ 1, 2, 3 ]; + strings := data.map(Display::fmt).collect::(); +}; diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6d31ae2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,77 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "import-tree": { + "locked": { + "lastModified": 1773693634, + "narHash": "sha256-BtZ2dtkBdSUnFPPFc+n0kcMbgaTxzFNPv2iaO326Ffg=", + "owner": "vic", + "repo": "import-tree", + "rev": "c41e7d58045f9057880b0d85e1152d6a4430dbf1", + "type": "github" + }, + "original": { + "owner": "vic", + "repo": "import-tree", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1774748309, + "narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "333c4e0545a6da976206c74db8773a1645b5870a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "import-tree": "import-tree", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..3d702f9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,11 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + flake-parts.url = "github:hercules-ci/flake-parts"; + import-tree.url = "github:vic/import-tree"; + }; + + outputs = + inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./nix-modules); +} diff --git a/nix-modules/packages/boilerplate/boilerplate.py b/nix-modules/packages/boilerplate/boilerplate.py new file mode 100644 index 0000000..a6b498b --- /dev/null +++ b/nix-modules/packages/boilerplate/boilerplate.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +import textwrap +from pathlib import Path + + +KIND_LAYOUTS = { + "layer": Path("nix-modules/features/layers"), + "feature": Path("nix-modules/features"), + "host": Path("nix-modules/hosts"), + "package": Path("nix-modules/packages"), +} + + +NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$") + + +def nix_string(value: str) -> str: + return json.dumps(value) + + +def git_root() -> Path | None: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + return None + return Path(result.stdout.strip()) + + +def host_templates(name: str) -> dict[Path, str]: + return { + Path("default.nix"): + textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosConfigurations.{name} = inputs.nixpkgs.lib.nixosSystem {{ + system = builtins.currentSystem; + modules = [ + self.nixosModules.{name}Configuration + ]; + }}; + }} + """ + ), + Path("configuration.nix"): + textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosModules.{name}Configuration = + {{ pkgs, lib, ... }}: + {{ + imports = [ + self.nixosModules.{name}Hardware + ]; + system.stateVersion = "25.11"; + }}; + }} + """ + ), + Path("hardware-configuration.nix"): + textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosModules.{name}Hardware = + {{ lib, pkgs, ... }}: + {{ + config = {{ + # If you're building a full system here, you'd likely want to just copy the hardware + # configuration generated by the NixOS installer. I've added some stuff here just so + # `nix flake check` passes on initialization of the flake and to show how it _could_ + # look. You can find it here: `/etc/nixos/hardware-configuration.nix` + + boot.loader.grub.devices = [ "/dev/sda" ]; + + fileSystems."/" = {{ + device = "tmpfs"; + fsType = "tmpfs"; + }}; + }}; + }}; + }} + """ + ), + } + + +def template(kind: str, name: str) -> str: + if kind in {"layer", "feature"}: + return textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosModules.{name} = {{ pkgs, lib, ... }}: + {{ + config = {{ + }}; + }}; + }} + """ + ) + + if kind == "host": + quoted_name = nix_string(name) + return textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + flake.nixosModules.{name} = {{ pkgs, lib, ... }}: + {{ + config = {{ + networking.hostName = {quoted_name}; + # system.stateVersion = "25.11"; + }}; + }}; + + flake.nixosConfigurations.{name} = inputs.nixpkgs.lib.nixosSystem {{ + modules = [ + self.nixosModules.{name} + ]; + }}; + }} + """ + ) + + if kind == "package": + quoted_name = nix_string(name) + return textwrap.dedent( + f"""\ + {{ self, inputs, ... }}: + {{ + perSystem = + {{ pkgs, ... }}: + {{ + packages.{name} = pkgs.writeShellApplication {{ + name = {quoted_name}; + runtimeInputs = [ ]; + text = '' + echo "TODO: implement {name}" + ''; + }}; + }}; + }} + """ + ) + + raise ValueError(f"Unsupported kind: {kind}") + + +def planned_files(kind: str, name: str, multifile: bool) -> dict[Path, str]: + if kind == "host": + return host_templates(name) + + target = Path("default.nix") if multifile else Path(f"{name}.nix") + return {target: template(kind, name)} + + +def write_file(path: Path, content: str) -> None: + if path.exists(): + raise FileExistsError(f"Refusing to overwrite existing path: {path}") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content.rstrip() + "\n", encoding="utf-8") + + +def stage(paths: list[Path], root: Path) -> None: + subprocess.run( + ["git", "add", "--", *[str(path.relative_to(root)) for path in paths]], + check=True, + cwd=root, + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate Nix boilerplate modules") + parser.add_argument("kind", choices=sorted(KIND_LAYOUTS)) + parser.add_argument("name") + parser.add_argument("--no-git-stage", action="store_true", help="Do not stage created files") + parser.add_argument("--dry-run", action="store_true", help="Print planned files without creating them") + parser.add_argument( + "--multifile", + action="store_true", + help="Create /default.nix instead of .nix", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + + if not NAME_RE.fullmatch(args.name): + print( + "name must match ^[A-Za-z0-9][A-Za-z0-9_.-]*$", + file=sys.stderr, + ) + return 2 + + root = git_root() + if root is None: + if args.dry_run or args.no_git_stage: + root = Path.cwd() + else: + print("not inside a git repository; use --no-git-stage", file=sys.stderr) + return 2 + + files = planned_files(args.kind, args.name, args.multifile) + base = root / KIND_LAYOUTS[args.kind] + target_base = base / args.name if args.kind == "host" else base + targets = [target_base / rel for rel in files] + + if args.dry_run: + print("dry run: no files will be created") + for target in targets: + print(target.relative_to(root)) + return 0 + + try: + for rel_path, content in files.items(): + write_file(target_base / rel_path, content) + except FileExistsError as err: + print(err, file=sys.stderr) + return 1 + + if not args.no_git_stage: + stage(targets, root) + + for target in targets: + print(target.relative_to(root)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/nix-modules/packages/boilerplate/default.nix b/nix-modules/packages/boilerplate/default.nix new file mode 100644 index 0000000..9498389 --- /dev/null +++ b/nix-modules/packages/boilerplate/default.nix @@ -0,0 +1,32 @@ +{ self, inputs, ... }: +{ + perSystem = + { pkgs, lib, ... }: + { + packages.boilerplate = pkgs.stdenvNoCC.mkDerivation { + pname = "boilerplate"; + version = "0.1.0"; + + src = ./.; + + dontConfigure = true; + dontBuild = true; + + installPhase = '' + runHook preInstall + + substituteInPlace boilerplate.py \ + --replace-fail '#!/usr/bin/env python3' '#!${pkgs.python3}/bin/python3' + install -Dm755 boilerplate.py "$out/bin/boilerplate" + + runHook postInstall + ''; + + meta = { + description = "Generate boilerplate Nix modules"; + license = lib.licenses.mit; + mainProgram = "boilerplate"; + }; + }; + }; +} diff --git a/nix-modules/parts.nix b/nix-modules/parts.nix new file mode 100644 index 0000000..657e3ca --- /dev/null +++ b/nix-modules/parts.nix @@ -0,0 +1,10 @@ +{ + config = { + systems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-linux" + "aarch64-darwin" + ]; + }; +} diff --git a/nix-modules/shell.nix b/nix-modules/shell.nix new file mode 100644 index 0000000..baffcb3 --- /dev/null +++ b/nix-modules/shell.nix @@ -0,0 +1,26 @@ +{ ... }: +{ + perSystem = + { pkgs, self', ... }: + { + # An example devshell with some Rust and Nix tools + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + bacon + cargo + rust-analyzer + rustc + rustfmt + clippy + glibc + sea-orm-cli + nixfmt + nil + alejandra + self'.packages.boilerplate + ]; + nativeBuildInputs = [ pkgs.pkg-config ]; + env.RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + }; + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2764e52 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,6 @@ +mod parser; +mod token; + +fn main() { + println!("Hello, world!"); +} diff --git a/src/parser/ast.rs b/src/parser/ast.rs new file mode 100644 index 0000000..f428438 --- /dev/null +++ b/src/parser/ast.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; + +pub struct Ast { + program: Vec, +} + +pub enum Statement { + Open(String), // TODO: parse into `OpenTree` + Definition { + is_public: bool, + ident: String, + generics: Vec, // TODO: refine + definition_kind: crate::token::Kind, + definition: Expression, + }, + Module { + is_public: bool, + ident: String, + definition: Vec, + }, + Comment(String), // TODO: differentiate kinds +} + +pub enum Expression { + Function { + args: Vec, // TODO: get more granular + body: Vec, + }, + Application { + function: Box, + args: Vec, + }, + BinOp { + operands: Box<(Expression, Expression)>, + operator: Box, // TODO: refine? (maybe we _want_ arbitrary binary operators) + }, + Type(Vec), + GenericSpecification(Vec), // TODO: refine +} + +pub enum Type { + Record(HashMap), // { red: u8, green: u8, blue: u8 } + Tuple(Vec), // `Some (T) | None` + Simple(String), // `Option::Some (T)` + Empty, // `Option::None` +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs new file mode 100644 index 0000000..94779fa --- /dev/null +++ b/src/parser/mod.rs @@ -0,0 +1,31 @@ +use chumsky::prelude::*; + +use crate::token::{Kind, Token}; + +pub mod ast; + +type Span = SimpleSpan; +type Spanned = (T, Span); + +fn tok<'a>(kind: Kind) -> impl Parser<'a, &'a [Token], (), extra::Err>> { + any().filter(move |t: &Token| t.kind == kind).ignored() +} + +fn parser<'a>() -> impl Parser<'a, &'a [Token], i64, extra::Err>> { + recursive(|expr| { + // let ident = select! { Kind::Ident(n) => n }; + + let args = expr + .recover_with(skip_then_retry_until( + any().ignored(), + any() + .filter(|t: &Token| matches!(t.kind, Kind::Comma | Kind::RParen)) + .ignored(), + )) + .separated_by(tok(Kind::Comma)) + .collect::>() + .delimited_by(tok(Kind::LParen), tok(Kind::RParen)); + + args + }) +} diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..0629366 --- /dev/null +++ b/src/token.rs @@ -0,0 +1,184 @@ +use logos::{Lexer, Logos}; + +#[derive(Clone, Default, Debug, PartialEq)] +pub struct TokenExtras { + line: usize, + line_start: usize, +} + +#[derive(Clone, Logos, Debug, PartialEq)] +#[logos(skip r"[ \t\r\n\f]+", extras = TokenExtras)] +pub enum Kind { + // special, idents, literals + #[regex(r#"([_a-zA-Z][a-zA-Z0-9]*)"#, |lex| lex.slice().to_owned())] + Ident(String), + #[regex(r#"([0-9]+)"#, |lex| lex.slice().to_owned())] + Integer(String), + // Float(f64), + #[regex(r#""([^"\\]|\\["\\bnfrt]|u[a-fA-F0-9]{4})*""#, |lex| lex.slice().get(1..lex.slice().len()-1).unwrap().to_owned())] + StringLiteral(String), // TODO: string interpolation will likely require this be changed + + #[regex(r#"//![\s]*(.*)"#, |lex| lex.slice().to_owned(), allow_greedy=true)] + ModDocComment(String), + #[regex(r#"///[\s]*(.*)"#, |lex| lex.slice().to_owned(), allow_greedy=true)] + DocComment(String), + #[regex(r#"//[\s]*(.*)"#, |lex| lex.slice().to_owned(), allow_greedy=true)] + InLineComment(String), + + // keywords + #[token("fn")] + Fn, + #[token("pub")] + Pub, + #[token("open")] + Open, + #[token("module")] + Module, + + // equality + #[token("=")] + EqualByAssign, + #[token(":=")] + EqualByDefinition, + #[token("::=")] + EqualByStructure, + + // syntax + #[token("(")] + LParen, + #[token(")")] + RParen, + #[token("{")] + LBrace, + #[token("}")] + RBrace, + #[token("[")] + LBracket, + #[token("]")] + RBracket, + #[token(",")] + Comma, + #[token(".")] + Dot, + #[token(";")] + Semicolon, + #[token("::")] + DoubleColon, + #[token("...")] + Ellipsis, + #[token("<")] + LAngle, + #[token(">")] + RAngle, + #[token("|")] + Bar, + #[token("->")] + Arrow, +} + +pub struct SpannedToken { + pub kind: Result, + pub line: usize, + pub column: usize, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Token { + pub kind: Kind, + pub line: usize, + pub column: usize, +} + +impl TryFrom for Token { + type Error = (); + fn try_from(SpannedToken { kind, line, column }: SpannedToken) -> Result { + Ok(Self { + kind: kind?, + line, + column, + }) + } +} + +pub struct SpannedLexer<'src> { + inner: Lexer<'src, Kind>, +} + +impl<'src> SpannedLexer<'src> { + pub fn new(source: &'src str) -> Self { + Self { + inner: Kind::lexer(source), + } + } +} + +impl Iterator for SpannedLexer<'_> { + type Item = SpannedToken; + + fn next(&mut self) -> Option { + let kind = self.inner.next()?; + let span = self.inner.span(); + let line = self.inner.extras.line; + let column = span.start - self.inner.extras.line_start; + + Some(SpannedToken { kind, line, column }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn spans() -> Vec { + vec![ + Token { + kind: Kind::ModDocComment("//! Module-level documentation".into()), + line: 0, + column: 0, + }, + Token { + kind: Kind::Open, + line: 0, + column: 0, + }, + Token { + kind: Kind::Ident("std".into()), + line: 0, + column: 0, + }, + Token { + kind: Kind::DoubleColon, + line: 0, + column: 0, + }, + Token { + kind: Kind::Ident("io".into()), + line: 0, + column: 0, + }, + ] + } + + #[test] + fn can_lex_example_main() { + let mut lexer = SpannedLexer::new(include_str!("../examples/main.jkl")); + for span in spans() { + let next: Token = lexer + .next() + .expect("Should be Some") + .try_into() + .expect("Should be Ok"); + assert_eq!(next.kind, span.kind); + } + + while let Some(span) = lexer.next() { + dbg!(&span.kind); + if span.kind.is_err() { + println!("Err token found at line={} col={}", span.line, span.column); + } + span.kind.expect("Should be valid token"); + } + + assert!(lexer.next().is_none()); + } +}