peppermint-lang 0.1.0__tar.gz
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.
- peppermint_lang-0.1.0/PKG-INFO +74 -0
- peppermint_lang-0.1.0/README.md +45 -0
- peppermint_lang-0.1.0/peppermint/__init__.py +0 -0
- peppermint_lang-0.1.0/peppermint/__main__.py +162 -0
- peppermint_lang-0.1.0/peppermint/ast_nodes.py +207 -0
- peppermint_lang-0.1.0/peppermint/bridge.py +220 -0
- peppermint_lang-0.1.0/peppermint/diagnostics.py +70 -0
- peppermint_lang-0.1.0/peppermint/interpreter.py +532 -0
- peppermint_lang-0.1.0/peppermint/libs/__init__.py +0 -0
- peppermint_lang-0.1.0/peppermint/libs/math_.py +41 -0
- peppermint_lang-0.1.0/peppermint/libs/ml.py +150 -0
- peppermint_lang-0.1.0/peppermint/libs/str_.py +72 -0
- peppermint_lang-0.1.0/peppermint/libs/viz.py +129 -0
- peppermint_lang-0.1.0/peppermint/parser.py +635 -0
- peppermint_lang-0.1.0/peppermint/stdlib/__init__.py +28 -0
- peppermint_lang-0.1.0/peppermint/stdlib/core.py +288 -0
- peppermint_lang-0.1.0/peppermint_lang.egg-info/PKG-INFO +74 -0
- peppermint_lang-0.1.0/peppermint_lang.egg-info/SOURCES.txt +25 -0
- peppermint_lang-0.1.0/peppermint_lang.egg-info/dependency_links.txt +1 -0
- peppermint_lang-0.1.0/peppermint_lang.egg-info/entry_points.txt +2 -0
- peppermint_lang-0.1.0/peppermint_lang.egg-info/requires.txt +20 -0
- peppermint_lang-0.1.0/peppermint_lang.egg-info/top_level.txt +1 -0
- peppermint_lang-0.1.0/pyproject.toml +34 -0
- peppermint_lang-0.1.0/setup.cfg +4 -0
- peppermint_lang-0.1.0/tests/test_grammar.py +332 -0
- peppermint_lang-0.1.0/tests/test_interpreter.py +232 -0
- peppermint_lang-0.1.0/tests/test_parser.py +431 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: peppermint-lang
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pipe-first DSL for data and ML work
|
|
5
|
+
Author-email: Chayapatr Archiwaranguprok <pub@mit.edu>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/chayapatr/peppermint
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Provides-Extra: data
|
|
14
|
+
Requires-Dist: pandas; extra == "data"
|
|
15
|
+
Provides-Extra: ml
|
|
16
|
+
Requires-Dist: pandas; extra == "ml"
|
|
17
|
+
Requires-Dist: scikit-learn; extra == "ml"
|
|
18
|
+
Requires-Dist: umap-learn; extra == "ml"
|
|
19
|
+
Provides-Extra: viz
|
|
20
|
+
Requires-Dist: pandas; extra == "viz"
|
|
21
|
+
Requires-Dist: matplotlib; extra == "viz"
|
|
22
|
+
Requires-Dist: seaborn; extra == "viz"
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: pandas; extra == "all"
|
|
25
|
+
Requires-Dist: scikit-learn; extra == "all"
|
|
26
|
+
Requires-Dist: umap-learn; extra == "all"
|
|
27
|
+
Requires-Dist: matplotlib; extra == "all"
|
|
28
|
+
Requires-Dist: seaborn; extra == "all"
|
|
29
|
+
|
|
30
|
+
# Peppermint
|
|
31
|
+
|
|
32
|
+
A pipe-first DSL for data and ML work. Designed to be lightweight and readable, where every operation is a pipeline step, errors propagate automatically, and the heavy lifting happens internally so you don't have to worry about it.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Run
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pep file.pep # run a file
|
|
44
|
+
pep # interactive REPL
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Example
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
load("survey.csv")
|
|
51
|
+
|> filter(it.age > 18)
|
|
52
|
+
|> add(score: it.income / it.age)
|
|
53
|
+
|> sort(by: "score", dir: "desc")
|
|
54
|
+
|> print()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
load("survey.csv")
|
|
59
|
+
|> group(by: "region") {
|
|
60
|
+
|> agg(avg_score: mean(it.score), n: count())
|
|
61
|
+
}
|
|
62
|
+
|> sort(by: "avg_score", dir: "desc")
|
|
63
|
+
|> print()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Each step prints a summary as it runs:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
|> filter → List 843 rows × 5 cols (157 dropped)
|
|
70
|
+
|> add → List 843 rows × 6 cols (+score)
|
|
71
|
+
|> sort → List 843 rows × 6 cols
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
See [docs/language.md](docs/language.md) for the full language reference and [examples/](examples/) for more.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Peppermint
|
|
2
|
+
|
|
3
|
+
A pipe-first DSL for data and ML work. Designed to be lightweight and readable, where every operation is a pipeline step, errors propagate automatically, and the heavy lifting happens internally so you don't have to worry about it.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install -e .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Run
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pep file.pep # run a file
|
|
15
|
+
pep # interactive REPL
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
load("survey.csv")
|
|
22
|
+
|> filter(it.age > 18)
|
|
23
|
+
|> add(score: it.income / it.age)
|
|
24
|
+
|> sort(by: "score", dir: "desc")
|
|
25
|
+
|> print()
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
load("survey.csv")
|
|
30
|
+
|> group(by: "region") {
|
|
31
|
+
|> agg(avg_score: mean(it.score), n: count())
|
|
32
|
+
}
|
|
33
|
+
|> sort(by: "avg_score", dir: "desc")
|
|
34
|
+
|> print()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each step prints a summary as it runs:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
|> filter → List 843 rows × 5 cols (157 dropped)
|
|
41
|
+
|> add → List 843 rows × 6 cols (+score)
|
|
42
|
+
|> sort → List 843 rows × 6 cols
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
See [docs/language.md](docs/language.md) for the full language reference and [examples/](examples/) for more.
|
|
File without changes
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
sys.setrecursionlimit(50000)
|
|
4
|
+
from .parser import parse, ParseError
|
|
5
|
+
from .interpreter import Interpreter, Err, Ok, PepError
|
|
6
|
+
from .stdlib import build_global_env
|
|
7
|
+
from .diagnostics import report_parse_error, report_pep_error, report_err
|
|
8
|
+
from .interpreter import ListValue, PmFunction, PmRange
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _repl_display(value):
|
|
12
|
+
if value is None:
|
|
13
|
+
return
|
|
14
|
+
if isinstance(value, bool):
|
|
15
|
+
val_str = "true" if value else "false"
|
|
16
|
+
type_str = "bool"
|
|
17
|
+
elif isinstance(value, int):
|
|
18
|
+
val_str = str(value)
|
|
19
|
+
type_str = "num"
|
|
20
|
+
elif isinstance(value, float):
|
|
21
|
+
val_str = str(value)
|
|
22
|
+
type_str = "num"
|
|
23
|
+
elif isinstance(value, str):
|
|
24
|
+
val_str = repr(value)
|
|
25
|
+
type_str = "str"
|
|
26
|
+
elif isinstance(value, ListValue):
|
|
27
|
+
val_str = repr(value)
|
|
28
|
+
type_str = ""
|
|
29
|
+
elif isinstance(value, PmFunction):
|
|
30
|
+
val_str = repr(value)
|
|
31
|
+
type_str = "fn"
|
|
32
|
+
elif isinstance(value, PmRange):
|
|
33
|
+
val_str = f"{value.start}..{value.end}"
|
|
34
|
+
type_str = "range"
|
|
35
|
+
elif isinstance(value, dict):
|
|
36
|
+
pairs = ", ".join(f"{k}: {v!r}" for k, v in value.items())
|
|
37
|
+
val_str = "{ " + pairs + " }"
|
|
38
|
+
type_str = "obj"
|
|
39
|
+
elif isinstance(value, list):
|
|
40
|
+
val_str = repr(value)
|
|
41
|
+
type_str = "list"
|
|
42
|
+
else:
|
|
43
|
+
val_str = str(value)
|
|
44
|
+
type_str = type(value).__name__
|
|
45
|
+
|
|
46
|
+
prefix = "\033[32m<<<\033[0m"
|
|
47
|
+
# "<<< " is 4 chars; pad so arrow aligns at col 34 (matching pipe step)
|
|
48
|
+
if type_str:
|
|
49
|
+
pad = max(0, 34 - 4 - len(val_str))
|
|
50
|
+
tag = f"\033[33m← {type_str}\033[0m"
|
|
51
|
+
print(f"{prefix} {val_str}{' ' * pad}\033[33m←\033[0m \033[33m{type_str}\033[0m")
|
|
52
|
+
else:
|
|
53
|
+
print(f"{prefix} {val_str}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_file(args):
|
|
57
|
+
try:
|
|
58
|
+
src = open(args.file).read()
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
print(f"\033[1;31mError:\033[0m × file not found: {args.file}", file=sys.stderr)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
program = parse(src)
|
|
65
|
+
except ParseError as e:
|
|
66
|
+
report_parse_error(e, src, args.file)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"\033[1;31mError:\033[0m × {e}", file=sys.stderr)
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
|
|
72
|
+
env = build_global_env()
|
|
73
|
+
interp = Interpreter(env, quiet=args.quiet)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
result = interp.run(program)
|
|
77
|
+
except PepError as e:
|
|
78
|
+
report_pep_error(e, src, args.file)
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"\033[1;31mError:\033[0m × {e}", file=sys.stderr)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
if isinstance(result, Err):
|
|
85
|
+
report_err(result, src, args.file)
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_repl(args):
|
|
90
|
+
import readline # enables arrow keys and history
|
|
91
|
+
env = build_global_env()
|
|
92
|
+
interp = Interpreter(env, quiet=False)
|
|
93
|
+
|
|
94
|
+
print("Peppermint REPL (Ctrl+D to exit)")
|
|
95
|
+
|
|
96
|
+
buf = []
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
prompt = "\033[2m...\033[0m " if buf else "\033[32m>>>\033[0m "
|
|
100
|
+
try:
|
|
101
|
+
line = input(prompt)
|
|
102
|
+
except EOFError:
|
|
103
|
+
print()
|
|
104
|
+
break
|
|
105
|
+
except KeyboardInterrupt:
|
|
106
|
+
print()
|
|
107
|
+
buf = []
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
stripped = line.rstrip()
|
|
111
|
+
buf.append(line)
|
|
112
|
+
|
|
113
|
+
# Try joining with newlines first, then with spaces (handles `1 +\n2` style)
|
|
114
|
+
program = None
|
|
115
|
+
for src in ("\n".join(buf), " ".join(buf)):
|
|
116
|
+
try:
|
|
117
|
+
program = parse(src)
|
|
118
|
+
break
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
if program is None:
|
|
123
|
+
if stripped == "" and len(buf) == 1:
|
|
124
|
+
buf = []
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
buf = []
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
result = interp.run(program)
|
|
131
|
+
except PepError as e:
|
|
132
|
+
report_pep_error(e, src, "<repl>") # src is still in scope from the loop above
|
|
133
|
+
continue
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"\033[1;31mError:\033[0m × {e}", file=sys.stderr)
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if result is None:
|
|
139
|
+
continue
|
|
140
|
+
if isinstance(result, Err):
|
|
141
|
+
report_err(result, src, "<repl>")
|
|
142
|
+
elif isinstance(result, Ok):
|
|
143
|
+
_repl_display(result.value)
|
|
144
|
+
else:
|
|
145
|
+
_repl_display(result)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main():
|
|
149
|
+
ap = argparse.ArgumentParser(prog="pep", description="Peppermint language")
|
|
150
|
+
ap.add_argument("file", nargs="?", help="Path to .pep file (omit to start REPL)")
|
|
151
|
+
ap.add_argument("--quiet", action="store_true", help="Suppress pipe step summaries")
|
|
152
|
+
|
|
153
|
+
args = ap.parse_args()
|
|
154
|
+
|
|
155
|
+
if args.file:
|
|
156
|
+
run_file(args)
|
|
157
|
+
else:
|
|
158
|
+
run_repl(args)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Loc:
|
|
8
|
+
line: int
|
|
9
|
+
col: int
|
|
10
|
+
|
|
11
|
+
def __repr__(self):
|
|
12
|
+
return f"{self.line}:{self.col}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
NO_LOC = Loc(0, 0)
|
|
16
|
+
|
|
17
|
+
# --- Patterns (used in match arms) ---
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PatComparison:
|
|
21
|
+
op: str # >, <, >=, <=, ==, !=
|
|
22
|
+
value: Any # int | float | str | Expr (variable)
|
|
23
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class PatOk:
|
|
27
|
+
name: str
|
|
28
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PatErr:
|
|
32
|
+
name: str
|
|
33
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class PatTuple:
|
|
37
|
+
patterns: list
|
|
38
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class PatWildcard:
|
|
42
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
43
|
+
|
|
44
|
+
Pattern = PatComparison | PatOk | PatErr | PatTuple | PatWildcard
|
|
45
|
+
|
|
46
|
+
# --- Expressions ---
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class IntLit:
|
|
50
|
+
value: int
|
|
51
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class FloatLit:
|
|
55
|
+
value: float
|
|
56
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class StrLit:
|
|
60
|
+
value: str
|
|
61
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class BoolLit:
|
|
65
|
+
value: bool
|
|
66
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class NoneLit:
|
|
70
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Ident:
|
|
74
|
+
name: str
|
|
75
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class FieldAccess:
|
|
79
|
+
obj: Any # Expr
|
|
80
|
+
field: str
|
|
81
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class Neg:
|
|
85
|
+
operand: Any # Expr
|
|
86
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class BinOp:
|
|
90
|
+
op: str # +, -, *, /, >, <, >=, <=, ==, !=
|
|
91
|
+
left: Any # Expr
|
|
92
|
+
right: Any # Expr
|
|
93
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Call:
|
|
97
|
+
func: Any # Expr — already resolved to FieldAccess or Ident by postfix rule
|
|
98
|
+
args: list # list[Expr]
|
|
99
|
+
kwargs: dict # str -> Expr
|
|
100
|
+
block: list | None # list[PipeStep] | None
|
|
101
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Lambda:
|
|
105
|
+
params: list[str]
|
|
106
|
+
body: Any # Expr
|
|
107
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class Pipe:
|
|
111
|
+
steps: list # list[Expr] — first is the source, rest are calls
|
|
112
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class PipeStep:
|
|
116
|
+
expr: Any # Call
|
|
117
|
+
quiet: bool
|
|
118
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class Match:
|
|
122
|
+
subject: Any # Expr
|
|
123
|
+
arms: list # list[MatchArm]
|
|
124
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class MatchArm:
|
|
128
|
+
pattern: Pattern
|
|
129
|
+
body: Any # Expr
|
|
130
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class ListLit:
|
|
134
|
+
items: list # list[Expr]
|
|
135
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class ObjField:
|
|
139
|
+
key: str
|
|
140
|
+
value: Any # Expr
|
|
141
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class ObjSpread:
|
|
145
|
+
obj: Any # Expr
|
|
146
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class ObjShorthand:
|
|
150
|
+
key: str # { x } = { x: x }
|
|
151
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class ObjLit:
|
|
155
|
+
entries: list # list[ObjField | ObjSpread]
|
|
156
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class TupleLit:
|
|
160
|
+
items: list # list[Expr]
|
|
161
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class Spread:
|
|
165
|
+
obj: Any # Expr
|
|
166
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class Range:
|
|
170
|
+
start: int
|
|
171
|
+
end: int
|
|
172
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class Block:
|
|
176
|
+
stmts: list # list[Expr] — evaluates each, returns last
|
|
177
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class Literal:
|
|
181
|
+
value: Any # already-evaluated runtime value, passes through eval unchanged
|
|
182
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
183
|
+
|
|
184
|
+
# --- Statements ---
|
|
185
|
+
|
|
186
|
+
@dataclass
|
|
187
|
+
class Assign:
|
|
188
|
+
name: str
|
|
189
|
+
value: Any # Expr
|
|
190
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class UseDecl:
|
|
194
|
+
path: str # module name or file path string
|
|
195
|
+
alias: str | None
|
|
196
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class NsDecl:
|
|
200
|
+
name: str
|
|
201
|
+
body: list # list[Assign]
|
|
202
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class Program:
|
|
206
|
+
body: list # list[Assign | UseDecl | NsDecl | Expr]
|
|
207
|
+
loc: Loc = field(default_factory=lambda: NO_LOC)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Peppermint ↔ Python bridge.
|
|
3
|
+
|
|
4
|
+
Python libraries loaded via `use` should import from here rather than
|
|
5
|
+
touching interpreter internals directly. The bridge is the single place
|
|
6
|
+
that knows about both worlds.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
import functools
|
|
11
|
+
|
|
12
|
+
# Single cached tuple — atomic assignment avoids the partial-init race
|
|
13
|
+
_types: tuple | None = None
|
|
14
|
+
|
|
15
|
+
def _interp_types():
|
|
16
|
+
global _types
|
|
17
|
+
if _types is None:
|
|
18
|
+
from .interpreter import Ok, Err, ListValue
|
|
19
|
+
_types = (Ok, Err, ListValue)
|
|
20
|
+
return _types
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --- Type predicates ---
|
|
24
|
+
|
|
25
|
+
def is_ok(val) -> bool:
|
|
26
|
+
Ok, Err, ListValue = _interp_types()
|
|
27
|
+
return isinstance(val, Ok)
|
|
28
|
+
|
|
29
|
+
def is_err(val) -> bool:
|
|
30
|
+
Ok, Err, ListValue = _interp_types()
|
|
31
|
+
return isinstance(val, Err)
|
|
32
|
+
|
|
33
|
+
def is_list(val) -> bool:
|
|
34
|
+
Ok, Err, ListValue = _interp_types()
|
|
35
|
+
return isinstance(val, (ListValue, list))
|
|
36
|
+
|
|
37
|
+
def is_object_list(val) -> bool:
|
|
38
|
+
Ok, Err, ListValue = _interp_types()
|
|
39
|
+
if isinstance(val, ListValue):
|
|
40
|
+
return True
|
|
41
|
+
if isinstance(val, list):
|
|
42
|
+
return all(isinstance(el, dict) for el in val)
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --- Conversion: Peppermint → Python ---
|
|
47
|
+
|
|
48
|
+
def to_python(val) -> Any:
|
|
49
|
+
"""Unwrap Peppermint runtime values to plain Python.
|
|
50
|
+
|
|
51
|
+
Never raises — Err is returned as its message string so callers
|
|
52
|
+
don't need to handle two failure modes.
|
|
53
|
+
"""
|
|
54
|
+
Ok, Err, ListValue = _interp_types()
|
|
55
|
+
if isinstance(val, Ok):
|
|
56
|
+
return to_python(val.value)
|
|
57
|
+
if isinstance(val, Err):
|
|
58
|
+
return val.msg # caller decides what to do with it
|
|
59
|
+
if isinstance(val, ListValue):
|
|
60
|
+
return [to_python(r) for r in val.rows]
|
|
61
|
+
if isinstance(val, list):
|
|
62
|
+
return [to_python(r) for r in val]
|
|
63
|
+
try:
|
|
64
|
+
import numpy as np
|
|
65
|
+
if isinstance(val, np.integer): return int(val)
|
|
66
|
+
if isinstance(val, np.floating): return float(val)
|
|
67
|
+
if isinstance(val, np.ndarray): return val.tolist()
|
|
68
|
+
except ImportError:
|
|
69
|
+
pass
|
|
70
|
+
try:
|
|
71
|
+
import pandas as pd
|
|
72
|
+
if isinstance(val, pd.DataFrame): return val.to_dict(orient="records")
|
|
73
|
+
if isinstance(val, pd.Series): return val.tolist()
|
|
74
|
+
except ImportError:
|
|
75
|
+
pass
|
|
76
|
+
return val
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# --- Conversion: Python → Peppermint ---
|
|
80
|
+
|
|
81
|
+
def _infer_schema(rows: list[dict]) -> dict:
|
|
82
|
+
schema: dict = {}
|
|
83
|
+
unknown: set = set()
|
|
84
|
+
for row in rows:
|
|
85
|
+
for k, v in row.items():
|
|
86
|
+
if k in schema:
|
|
87
|
+
continue
|
|
88
|
+
if v is not None:
|
|
89
|
+
schema[k] = type(v)
|
|
90
|
+
else:
|
|
91
|
+
unknown.add(k)
|
|
92
|
+
# columns that were None in every row get type NoneType
|
|
93
|
+
for k in unknown - schema.keys():
|
|
94
|
+
schema[k] = type(None)
|
|
95
|
+
return schema
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def make_list(rows: list[dict]):
|
|
99
|
+
"""Wrap a list of dicts into a Peppermint ListValue."""
|
|
100
|
+
Ok, Err, ListValue = _interp_types()
|
|
101
|
+
return ListValue(rows=rows, schema=_infer_schema(rows))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _normalize(val) -> Any:
|
|
105
|
+
"""Coerce numpy/pandas types and unwrap Ok; never recurses into ListValue."""
|
|
106
|
+
Ok, Err, ListValue = _interp_types()
|
|
107
|
+
if isinstance(val, Ok):
|
|
108
|
+
return _normalize(val.value)
|
|
109
|
+
if isinstance(val, ListValue):
|
|
110
|
+
return val # preserve as-is; from_python will wrap in Ok
|
|
111
|
+
try:
|
|
112
|
+
import numpy as np
|
|
113
|
+
if isinstance(val, np.integer): return int(val)
|
|
114
|
+
if isinstance(val, np.floating): return float(val)
|
|
115
|
+
if isinstance(val, np.ndarray): return val.tolist()
|
|
116
|
+
except ImportError:
|
|
117
|
+
pass
|
|
118
|
+
try:
|
|
119
|
+
import pandas as pd
|
|
120
|
+
if isinstance(val, pd.DataFrame): return val.to_dict(orient="records")
|
|
121
|
+
if isinstance(val, pd.Series): return val.tolist()
|
|
122
|
+
except ImportError:
|
|
123
|
+
pass
|
|
124
|
+
return val
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def from_python(val) -> Any:
|
|
128
|
+
"""Wrap a plain Python value into a Peppermint Ok result."""
|
|
129
|
+
Ok, Err, ListValue = _interp_types()
|
|
130
|
+
val = _normalize(val)
|
|
131
|
+
if isinstance(val, ListValue):
|
|
132
|
+
return Ok(val) # already tabular, schema intact
|
|
133
|
+
if isinstance(val, list):
|
|
134
|
+
if all(isinstance(el, dict) for el in val):
|
|
135
|
+
return Ok(make_list(val))
|
|
136
|
+
return Ok(val) # scalar list — not tabular data
|
|
137
|
+
return Ok(val)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def err(msg: str):
|
|
141
|
+
"""Return a Peppermint Err."""
|
|
142
|
+
Ok, Err, ListValue = _interp_types()
|
|
143
|
+
return Err(msg)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def ok(val):
|
|
147
|
+
"""Return a Peppermint Ok."""
|
|
148
|
+
Ok, Err, ListValue = _interp_types()
|
|
149
|
+
return Ok(val)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# --- Row utilities ---
|
|
153
|
+
|
|
154
|
+
def get_rows(val) -> list[dict]:
|
|
155
|
+
"""Extract rows from a ListValue or plain list[dict]."""
|
|
156
|
+
Ok, Err, ListValue = _interp_types()
|
|
157
|
+
if isinstance(val, ListValue):
|
|
158
|
+
return val.rows
|
|
159
|
+
if isinstance(val, list):
|
|
160
|
+
return val
|
|
161
|
+
raise TypeError(f"expected a list, got {type(val).__name__}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def map_rows(val, fn: Callable[[dict], dict]):
|
|
165
|
+
"""Apply fn to each row, return a new ListValue wrapped in Ok."""
|
|
166
|
+
rows = get_rows(val)
|
|
167
|
+
return from_python([fn(row) for row in rows])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def add_column(val, name: str, fn: Callable[[dict], Any]):
|
|
171
|
+
"""Add a new field to every row using fn(row) -> value."""
|
|
172
|
+
rows = get_rows(val)
|
|
173
|
+
return from_python([{**row, name: fn(row)} for row in rows])
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def filter_rows(val, fn: Callable[[dict], bool]):
|
|
177
|
+
"""Keep rows where fn(row) is truthy."""
|
|
178
|
+
rows = get_rows(val)
|
|
179
|
+
return from_python([row for row in rows if fn(row)])
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# --- Library loader ---
|
|
183
|
+
|
|
184
|
+
def load_python_file(path: str, alias: str | None = None) -> dict:
|
|
185
|
+
"""Import a .py file and return its public functions wrapped for Peppermint."""
|
|
186
|
+
import importlib.util, inspect
|
|
187
|
+
|
|
188
|
+
spec = importlib.util.spec_from_file_location("_pep_user_lib", path)
|
|
189
|
+
mod = importlib.util.module_from_spec(spec)
|
|
190
|
+
spec.loader.exec_module(mod)
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
name: wrap(obj)
|
|
194
|
+
for name, obj in inspect.getmembers(mod, inspect.isfunction)
|
|
195
|
+
if not name.startswith("_")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def wrap(fn: Callable) -> Callable:
|
|
200
|
+
"""Wrap a plain Python function for use in Peppermint.
|
|
201
|
+
|
|
202
|
+
- Args converted from Peppermint values to plain Python
|
|
203
|
+
- Return value wrapped in Ok
|
|
204
|
+
- Exceptions become Err
|
|
205
|
+
"""
|
|
206
|
+
@functools.wraps(fn)
|
|
207
|
+
def wrapper(*args, _interp=None, _env=None, _block=None, **kwargs):
|
|
208
|
+
try:
|
|
209
|
+
converted = [to_python(a) for a in args]
|
|
210
|
+
converted_kwargs = {k: to_python(v) for k, v in kwargs.items()}
|
|
211
|
+
return from_python(fn(*converted, **converted_kwargs))
|
|
212
|
+
except Exception as e:
|
|
213
|
+
Ok, Err, ListValue = _interp_types()
|
|
214
|
+
return Err(str(e))
|
|
215
|
+
return wrapper
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def wrap_lib(fns: dict) -> dict:
|
|
219
|
+
"""Wrap a dict of plain Python functions for use in Peppermint."""
|
|
220
|
+
return {name: wrap(fn) for name, fn in fns.items()}
|