This commit is contained in:
jingus 2026-04-19 04:03:00 -05:00
commit 34e45e702f
16 changed files with 1109 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.direnv/

370
Cargo.lock generated Normal file
View file

@ -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",
]

9
Cargo.toml Normal file
View file

@ -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"

42
README.md Normal file
View file

@ -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<T, E> ::= Ok (T) | Err (E);
Option<T> ::= 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.

17
examples/main.jkl Normal file
View file

@ -0,0 +1,17 @@
//! Module-level documentation
open std::io;
Result<T, E> ::= 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::<List<...>();
};

77
flake.lock generated Normal file
View file

@ -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
}

11
flake.nix Normal file
View file

@ -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);
}

View file

@ -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 <name>/default.nix instead of <name>.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())

View file

@ -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";
};
};
};
}

10
nix-modules/parts.nix Normal file
View file

@ -0,0 +1,10 @@
{
config = {
systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-linux"
"aarch64-darwin"
];
};
}

26
nix-modules/shell.nix Normal file
View file

@ -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}";
};
};
}

6
src/main.rs Normal file
View file

@ -0,0 +1,6 @@
mod parser;
mod token;
fn main() {
println!("Hello, world!");
}

46
src/parser/ast.rs Normal file
View file

@ -0,0 +1,46 @@
use std::collections::HashMap;
pub struct Ast {
program: Vec<Statement>,
}
pub enum Statement {
Open(String), // TODO: parse into `OpenTree`
Definition {
is_public: bool,
ident: String,
generics: Vec<Expression>, // TODO: refine
definition_kind: crate::token::Kind,
definition: Expression,
},
Module {
is_public: bool,
ident: String,
definition: Vec<Statement>,
},
Comment(String), // TODO: differentiate kinds
}
pub enum Expression {
Function {
args: Vec<Expression>, // TODO: get more granular
body: Vec<Statement>,
},
Application {
function: Box<Expression>,
args: Vec<Expression>,
},
BinOp {
operands: Box<(Expression, Expression)>,
operator: Box<Expression>, // TODO: refine? (maybe we _want_ arbitrary binary operators)
},
Type(Vec<Type>),
GenericSpecification(Vec<Expression>), // TODO: refine
}
pub enum Type {
Record(HashMap<String, String>), // { red: u8, green: u8, blue: u8 }
Tuple(Vec<String>), // `Some (T) | None`
Simple(String), // `Option::Some (T)`
Empty, // `Option::None`
}

31
src/parser/mod.rs Normal file
View file

@ -0,0 +1,31 @@
use chumsky::prelude::*;
use crate::token::{Kind, Token};
pub mod ast;
type Span = SimpleSpan;
type Spanned<T> = (T, Span);
fn tok<'a>(kind: Kind) -> impl Parser<'a, &'a [Token], (), extra::Err<Rich<'a, Token>>> {
any().filter(move |t: &Token| t.kind == kind).ignored()
}
fn parser<'a>() -> impl Parser<'a, &'a [Token], i64, extra::Err<Rich<'a, Token>>> {
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::<Vec<_>>()
.delimited_by(tok(Kind::LParen), tok(Kind::RParen));
args
})
}

184
src/token.rs Normal file
View file

@ -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<Kind, ()>,
pub line: usize,
pub column: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Token {
pub kind: Kind,
pub line: usize,
pub column: usize,
}
impl TryFrom<SpannedToken> for Token {
type Error = ();
fn try_from(SpannedToken { kind, line, column }: SpannedToken) -> Result<Self, Self::Error> {
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<Self::Item> {
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<Token> {
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());
}
}