haskell_match 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +98 -0
  3. data/LICENSE-APACHE +202 -0
  4. data/LICENSE-MIT +21 -0
  5. data/README.md +1484 -0
  6. data/ext/haskell_match/Cargo.lock +33 -0
  7. data/ext/haskell_match/Cargo.toml +22 -0
  8. data/ext/haskell_match/extconf.rb +41 -0
  9. data/ext/haskell_match/src/core/ast.rs +190 -0
  10. data/ext/haskell_match/src/core/error.rs +52 -0
  11. data/ext/haskell_match/src/core/exhaust.rs +699 -0
  12. data/ext/haskell_match/src/core/hs/ast.rs +256 -0
  13. data/ext/haskell_match/src/core/hs/json.rs +225 -0
  14. data/ext/haskell_match/src/core/hs/layout.rs +346 -0
  15. data/ext/haskell_match/src/core/hs/lexer.rs +688 -0
  16. data/ext/haskell_match/src/core/hs/mod.rs +14 -0
  17. data/ext/haskell_match/src/core/hs/parser.rs +1945 -0
  18. data/ext/haskell_match/src/core/lexer.rs +590 -0
  19. data/ext/haskell_match/src/core/mod.rs +19 -0
  20. data/ext/haskell_match/src/core/parser.rs +1116 -0
  21. data/ext/haskell_match/src/core/pretty.rs +373 -0
  22. data/ext/haskell_match/src/core/resolve.rs +336 -0
  23. data/ext/haskell_match/src/core/tree.rs +921 -0
  24. data/ext/haskell_match/src/core/typecheck.rs +226 -0
  25. data/ext/haskell_match/src/core/types.rs +404 -0
  26. data/ext/haskell_match/src/lib.rs +19 -0
  27. data/ext/haskell_match/src/ruby/mod.rs +1195 -0
  28. data/ext/haskell_match/src/ruby/runtime.rs +1045 -0
  29. data/lib/haskell_match/binding_plan.rb +84 -0
  30. data/lib/haskell_match/case_of.rb +71 -0
  31. data/lib/haskell_match/clauses.rb +354 -0
  32. data/lib/haskell_match/data.rb +417 -0
  33. data/lib/haskell_match/deep_call.rb +98 -0
  34. data/lib/haskell_match/deriving.rb +130 -0
  35. data/lib/haskell_match/dsl.rb +71 -0
  36. data/lib/haskell_match/errors.rb +85 -0
  37. data/lib/haskell_match/field_types.rb +140 -0
  38. data/lib/haskell_match/function.rb +240 -0
  39. data/lib/haskell_match/haskell/compiler.rb +961 -0
  40. data/lib/haskell_match/haskell.rb +326 -0
  41. data/lib/haskell_match/inspect.rb +45 -0
  42. data/lib/haskell_match/lazy_list.rb +210 -0
  43. data/lib/haskell_match/native_loader.rb +64 -0
  44. data/lib/haskell_match/pattern.rb +75 -0
  45. data/lib/haskell_match/pattern_ast.rb +394 -0
  46. data/lib/haskell_match/prelude.rb +448 -0
  47. data/lib/haskell_match/scope.rb +44 -0
  48. data/lib/haskell_match/version.rb +5 -0
  49. data/lib/haskell_match.rb +41 -0
  50. metadata +124 -0
@@ -0,0 +1,33 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "haskell_match"
7
+ version = "0.1.0"
8
+ dependencies = [
9
+ "lazy_static",
10
+ "rutie",
11
+ ]
12
+
13
+ [[package]]
14
+ name = "lazy_static"
15
+ version = "1.5.1"
16
+ source = "registry+https://github.com/rust-lang/crates.io-index"
17
+ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
18
+
19
+ [[package]]
20
+ name = "libc"
21
+ version = "0.2.190"
22
+ source = "registry+https://github.com/rust-lang/crates.io-index"
23
+ checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
24
+
25
+ [[package]]
26
+ name = "rutie"
27
+ version = "0.13.1"
28
+ source = "registry+https://github.com/rust-lang/crates.io-index"
29
+ checksum = "30d1529470ad0b69b81a6d0224632d00c01b46ef671452e24407512165b7ed84"
30
+ dependencies = [
31
+ "lazy_static",
32
+ "libc",
33
+ ]
@@ -0,0 +1,22 @@
1
+ [package]
2
+ name = "haskell_match"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ license = "MIT OR Apache-2.0"
6
+ description = "Haskell-style pattern matching for Ruby, compiled to decision trees in Rust"
7
+ publish = false
8
+
9
+ [lib]
10
+ name = "haskell_match"
11
+ crate-type = ["cdylib"]
12
+
13
+ [dependencies]
14
+ # Rutie 0.13 targets Ruby 3.x; Rutie 0.14 targets Ruby 4.0.
15
+ rutie = "0.13"
16
+ lazy_static = "1.4"
17
+
18
+ [profile.release]
19
+ opt-level = 3
20
+ lto = true
21
+ codegen-units = 1
22
+ panic = "unwind"
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Builds the Rust extension when the gem is installed. Instead of mkmf we
4
+ # emit a Makefile that delegates to cargo and copies the resulting library to
5
+ # the gem's lib/haskell_match/native directory.
6
+
7
+ require "rbconfig"
8
+ require "fileutils"
9
+
10
+ cargo = ENV["CARGO"] || "cargo"
11
+ unless system(cargo, "--version", out: File::NULL, err: File::NULL)
12
+ abort "haskell_match requires a Rust toolchain (cargo) to build. See https://rustup.rs"
13
+ end
14
+
15
+ lib_name = case RbConfig::CONFIG["host_os"]
16
+ when /darwin/ then "libhaskell_match.dylib"
17
+ when /mswin|mingw|cygwin/ then "haskell_match.dll"
18
+ else "libhaskell_match.so"
19
+ end
20
+
21
+ ext_dir = __dir__
22
+ native_dir = File.expand_path("../../lib/haskell_match/native", __dir__)
23
+ FileUtils.mkdir_p(native_dir)
24
+
25
+ File.write("Makefile", <<~MAKEFILE)
26
+ CARGO ?= #{cargo}
27
+ EXT_DIR = #{ext_dir}
28
+ NATIVE_DIR = #{native_dir}
29
+ LIB = #{lib_name}
30
+
31
+ all:
32
+ \tcd $(EXT_DIR) && $(CARGO) build --release
33
+ \tcp $(EXT_DIR)/target/release/$(LIB) $(NATIVE_DIR)/$(LIB)
34
+
35
+ install: all
36
+
37
+ clean:
38
+ \tcd $(EXT_DIR) && $(CARGO) clean
39
+
40
+ .PHONY: all install clean
41
+ MAKEFILE
@@ -0,0 +1,190 @@
1
+ //! Pattern syntax trees.
2
+ //!
3
+ //! `RawPat` is what the parser produces: constructor names are strings and
4
+ //! nothing has been checked. `Pat` is the resolved form used by the compiler:
5
+ //! constructors are `ConId`s, variables are indices into the clause's binding
6
+ //! table, and list/tuple/bool sugar has been desugared into constructors.
7
+
8
+ use std::fmt;
9
+
10
+ /// Literal patterns. Integer literals that do not fit in an `i64` are kept as
11
+ /// their canonical decimal text in `Big`. There is no string literal: as in
12
+ /// Haskell, `"abc"` is the list `['a', 'b', 'c']` and is desugared by the
13
+ /// parser; `Char` holds one character (matched against a one-character Ruby
14
+ /// String, or a character of a String viewed as a list).
15
+ #[derive(Clone, Debug, PartialEq)]
16
+ pub enum Lit {
17
+ Int(i64),
18
+ Big(String),
19
+ Float(f64),
20
+ Char(char),
21
+ Sym(String),
22
+ }
23
+
24
+ /// A hashable identity for a literal (floats compared by bit pattern after
25
+ /// normalising `-0.0` to `0.0`).
26
+ #[derive(Clone, Debug, PartialEq, Eq, Hash)]
27
+ pub enum LitKey {
28
+ Int(i64),
29
+ Big(String),
30
+ Float(u64),
31
+ Char(char),
32
+ Sym(String),
33
+ }
34
+
35
+ /// The "type" of a literal for the purposes of type checking columns. All
36
+ /// numeric literals share one type (like Haskell's `Num a => a` defaulting);
37
+ /// strings and symbols are separate.
38
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39
+ pub enum LitKind {
40
+ Num,
41
+ Char,
42
+ Sym,
43
+ }
44
+
45
+ impl LitKind {
46
+ pub fn name(self) -> &'static str {
47
+ match self {
48
+ LitKind::Num => "Num",
49
+ LitKind::Char => "Char",
50
+ LitKind::Sym => "Symbol",
51
+ }
52
+ }
53
+ }
54
+
55
+ impl Lit {
56
+ pub fn key(&self) -> LitKey {
57
+ match self {
58
+ Lit::Int(i) => LitKey::Int(*i),
59
+ Lit::Big(s) => LitKey::Big(s.clone()),
60
+ Lit::Float(f) => {
61
+ let f = if *f == 0.0 { 0.0 } else { *f };
62
+ LitKey::Float(f.to_bits())
63
+ }
64
+ Lit::Char(c) => LitKey::Char(*c),
65
+ Lit::Sym(s) => LitKey::Sym(s.clone()),
66
+ }
67
+ }
68
+
69
+ pub fn kind(&self) -> LitKind {
70
+ match self {
71
+ Lit::Int(_) | Lit::Big(_) | Lit::Float(_) => LitKind::Num,
72
+ Lit::Char(_) => LitKind::Char,
73
+ Lit::Sym(_) => LitKind::Sym,
74
+ }
75
+ }
76
+
77
+ pub fn same(&self, other: &Lit) -> bool {
78
+ self.key() == other.key()
79
+ }
80
+ }
81
+
82
+ impl fmt::Display for Lit {
83
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84
+ match self {
85
+ Lit::Int(i) => write!(f, "{}", i),
86
+ Lit::Big(s) => write!(f, "{}", s),
87
+ Lit::Float(x) => {
88
+ if x.fract() == 0.0 && x.is_finite() {
89
+ write!(f, "{:.1}", x)
90
+ } else {
91
+ write!(f, "{}", x)
92
+ }
93
+ }
94
+ Lit::Char(c) => write!(f, "{:?}", c),
95
+ Lit::Sym(s) => write!(f, ":{}", s),
96
+ }
97
+ }
98
+ }
99
+
100
+ /// A key of a Hash pattern: a Ruby Symbol or String.
101
+ #[derive(Clone, Debug, PartialEq, Eq, Hash)]
102
+ pub enum HKey {
103
+ Sym(String),
104
+ Str(String),
105
+ }
106
+
107
+ impl fmt::Display for HKey {
108
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109
+ match self {
110
+ HKey::Sym(s) => {
111
+ let plain = s
112
+ .chars()
113
+ .next()
114
+ .map(|c| c.is_ascii_lowercase() || c == '_')
115
+ .unwrap_or(false)
116
+ && s.chars()
117
+ .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '\'');
118
+ if plain {
119
+ write!(f, "{}", s)
120
+ } else {
121
+ write!(f, ":{:?}", s)
122
+ }
123
+ }
124
+ HKey::Str(s) => write!(f, "{:?}", s),
125
+ }
126
+ }
127
+ }
128
+
129
+ /// Unresolved pattern straight from the parser.
130
+ #[derive(Clone, Debug, PartialEq)]
131
+ pub enum RawPat {
132
+ Wild,
133
+ Var(String),
134
+ As(String, Box<RawPat>),
135
+ Lazy(Box<RawPat>),
136
+ Bang(Box<RawPat>),
137
+ /// `Con p1 p2 ...` (positional application; `Con` alone is nullary).
138
+ Con(String, Vec<RawPat>),
139
+ /// `Con { f1 = p1, f2, .. }` — the `bool` is whether `..` was present.
140
+ Record(String, Vec<(String, RawPat)>, bool),
141
+ /// `(p1, p2, ...)` with 0 or 2+ elements (`()` is unit).
142
+ Tuple(Vec<RawPat>),
143
+ /// `[p1, p2, ...]`
144
+ List(Vec<RawPat>),
145
+ /// `p1 : p2`
146
+ Cons(Box<RawPat>, Box<RawPat>),
147
+ Lit(Lit),
148
+ /// `{ name = p1, "key" = p2 }`: a Ruby Hash with (at least) these keys.
149
+ Hash(Vec<(HKey, RawPat)>),
150
+ }
151
+
152
+ pub type VarId = usize;
153
+
154
+ /// Resolved pattern.
155
+ #[derive(Clone, Debug, PartialEq)]
156
+ pub enum Pat {
157
+ Wild,
158
+ Var(VarId),
159
+ As(VarId, Box<Pat>),
160
+ /// Irrefutable pattern: never fails to match, bindings are extracted
161
+ /// (and may fail) only once the clause is selected.
162
+ Lazy(Box<Pat>),
163
+ Con(super::types::ConId, Vec<Pat>),
164
+ Lit(Lit),
165
+ /// A Hash having every listed key, whose values match the sub-patterns
166
+ /// (other keys are ignored, as fields not mentioned in a record pattern).
167
+ Hash(Vec<(HKey, Pat)>),
168
+ }
169
+
170
+ impl Pat {
171
+ /// Strip binders and laziness, leaving only the refutable skeleton.
172
+ pub fn skeleton(&self) -> Pat {
173
+ match self {
174
+ Pat::Wild | Pat::Var(_) | Pat::Lazy(_) => Pat::Wild,
175
+ Pat::As(_, p) => p.skeleton(),
176
+ Pat::Con(c, args) => Pat::Con(*c, args.iter().map(Pat::skeleton).collect()),
177
+ Pat::Lit(l) => Pat::Lit(l.clone()),
178
+ Pat::Hash(fields) => Pat::Hash(
179
+ fields
180
+ .iter()
181
+ .map(|(k, p)| (k.clone(), p.skeleton()))
182
+ .collect(),
183
+ ),
184
+ }
185
+ }
186
+
187
+ pub fn is_wild(&self) -> bool {
188
+ matches!(self, Pat::Wild)
189
+ }
190
+ }
@@ -0,0 +1,52 @@
1
+ use std::fmt;
2
+
3
+ /// Classification of a compile-time error. The Ruby layer maps each kind to a
4
+ /// dedicated exception class.
5
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
6
+ pub enum ErrorKind {
7
+ /// Malformed pattern text.
8
+ Syntax,
9
+ /// Constructor name that is not registered in the type environment.
10
+ UnknownConstructor,
11
+ /// Constructor applied to the wrong number of arguments.
12
+ Arity,
13
+ /// Patterns of different types in the same position.
14
+ Type,
15
+ /// The same variable bound twice in one clause.
16
+ DuplicateVariable,
17
+ /// Record pattern problems (unknown field, positional constructor used
18
+ /// with named fields, duplicate field).
19
+ Field,
20
+ /// Malformed `data` declaration.
21
+ DataDeclaration,
22
+ /// Clauses of one function with different numbers of arguments.
23
+ ClauseArity,
24
+ }
25
+
26
+ #[derive(Clone, Debug, PartialEq, Eq)]
27
+ pub struct CoreError {
28
+ pub kind: ErrorKind,
29
+ pub message: String,
30
+ }
31
+
32
+ impl CoreError {
33
+ pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
34
+ CoreError {
35
+ kind,
36
+ message: message.into(),
37
+ }
38
+ }
39
+ pub fn syntax(message: impl Into<String>) -> Self {
40
+ Self::new(ErrorKind::Syntax, message)
41
+ }
42
+ }
43
+
44
+ impl fmt::Display for CoreError {
45
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46
+ write!(f, "{}", self.message)
47
+ }
48
+ }
49
+
50
+ impl std::error::Error for CoreError {}
51
+
52
+ pub type Result<T> = std::result::Result<T, CoreError>;