re2vp 1.0.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.
- re2vp-1.0.0/PKG-INFO +11 -0
- re2vp-1.0.0/README.md +0 -0
- re2vp-1.0.0/pyproject.toml +31 -0
- re2vp-1.0.0/pyproject.toml.orig +30 -0
- re2vp-1.0.0/src/re2vp/__init__.py +7 -0
- re2vp-1.0.0/src/re2vp/cli.py +136 -0
- re2vp-1.0.0/src/re2vp/utils.py +70 -0
re2vp-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: re2vp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Varphi
|
|
6
|
+
Author-email: Varphi <support@varphi-lang.com>
|
|
7
|
+
Requires-Dist: greenery>=4.2.2
|
|
8
|
+
Requires-Dist: vp2py>=3.0.3
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
re2vp-1.0.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "re2vp"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"greenery>=4.2.2",
|
|
9
|
+
"vp2py>=3.0.3",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[[project.authors]]
|
|
13
|
+
name = "Varphi"
|
|
14
|
+
email = "support@varphi-lang.com"
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
re2vp = "re2vp.cli:main"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"black>=26.5.1",
|
|
26
|
+
"pillow>=12.3.0",
|
|
27
|
+
"pyinstaller>=6.22.2",
|
|
28
|
+
"pytest>=9.1.1",
|
|
29
|
+
"requests>=2.34.2",
|
|
30
|
+
"vermin>=1.8.0",
|
|
31
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "re2vp"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Varphi", email = "support@varphi-lang.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"greenery>=4.2.2",
|
|
12
|
+
"vp2py>=3.0.3",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
re2vp = "re2vp.cli:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"black>=26.5.1",
|
|
25
|
+
"pillow>=12.3.0",
|
|
26
|
+
"pyinstaller>=6.22.2",
|
|
27
|
+
"pytest>=9.1.1",
|
|
28
|
+
"requests>=2.34.2",
|
|
29
|
+
"vermin>=1.8.0",
|
|
30
|
+
]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import io
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import typer
|
|
7
|
+
from vp2py import VarphiToPythonCompiler
|
|
8
|
+
from .utils import compile_regex_to_varphi
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(add_completion=False)
|
|
11
|
+
|
|
12
|
+
class MultiLineTapeBuffer:
|
|
13
|
+
def __init__(self, payload: str):
|
|
14
|
+
self.step = 0
|
|
15
|
+
self.payload = payload
|
|
16
|
+
|
|
17
|
+
def readline(self, *args, **kwargs) -> str:
|
|
18
|
+
if self.step == 0: # The tape count
|
|
19
|
+
self.step += 1
|
|
20
|
+
return "1\n"
|
|
21
|
+
elif self.step == 1: # The first tape content
|
|
22
|
+
self.step += 1
|
|
23
|
+
return self.payload + "\n"
|
|
24
|
+
return ""
|
|
25
|
+
|
|
26
|
+
def isatty(self) -> bool:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
def timed_step(name: str, verbose: bool, func, *args, **kwargs):
|
|
30
|
+
"""Helper to cleanly run a step, print verbose logs, and measure execution time."""
|
|
31
|
+
if verbose:
|
|
32
|
+
typer.echo(f"{name}...", err=True)
|
|
33
|
+
t0 = time.perf_counter()
|
|
34
|
+
result = func(*args, **kwargs)
|
|
35
|
+
t1 = time.perf_counter()
|
|
36
|
+
if verbose:
|
|
37
|
+
typer.echo(f" Done in {t1 - t0:.4f} seconds.\n", err=True)
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
@app.command(
|
|
41
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
42
|
+
help="Compile a regex to Varphi, and optionally execute it against a payload."
|
|
43
|
+
)
|
|
44
|
+
def re2vp_command(
|
|
45
|
+
ctx: typer.Context,
|
|
46
|
+
regex: Optional[str] = typer.Argument(None, help="The regular expression."),
|
|
47
|
+
search: bool = typer.Option(False, "--search", "-s", help="Execute search mode against a payload."),
|
|
48
|
+
file: Optional[Path] = typer.Option(None, "--file", "-f", exists=True, dir_okay=False, help="Payload file."),
|
|
49
|
+
debug: bool = typer.Option(False, "--debug", help="Enable verbose step-by-step logging."),
|
|
50
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Print execution steps and timings."),
|
|
51
|
+
):
|
|
52
|
+
"""re2vp: Regex to Varphi Compiler."""
|
|
53
|
+
|
|
54
|
+
# Input resolution
|
|
55
|
+
if search and not regex:
|
|
56
|
+
typer.echo("Error: When using --search, the regex must be provided as an argument.", err=True)
|
|
57
|
+
raise typer.Exit(code=1)
|
|
58
|
+
|
|
59
|
+
source_regex = regex if regex else sys.stdin.read().strip()
|
|
60
|
+
if not source_regex:
|
|
61
|
+
typer.echo("Error: No regex provided.", err=True)
|
|
62
|
+
raise typer.Exit(code=1)
|
|
63
|
+
|
|
64
|
+
search_payload = None
|
|
65
|
+
if search:
|
|
66
|
+
search_payload = file.read_text(encoding="utf-8") if file else sys.stdin.read()
|
|
67
|
+
if not search_payload.strip():
|
|
68
|
+
typer.echo("Error: Search payload cannot be empty.", err=True)
|
|
69
|
+
raise typer.Exit(code=1)
|
|
70
|
+
|
|
71
|
+
# Compile regex to vp
|
|
72
|
+
try:
|
|
73
|
+
vp_source = timed_step("[1/3] Compiling Regex to Varphi IR", verbose, compile_regex_to_varphi, source_regex)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
typer.echo(f"Regex compilation error: {e}", err=True)
|
|
76
|
+
raise typer.Exit(code=1)
|
|
77
|
+
|
|
78
|
+
# If compile-only mode, output and exit
|
|
79
|
+
if not search:
|
|
80
|
+
typer.echo(vp_source)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# Compile vp to py
|
|
84
|
+
try:
|
|
85
|
+
compiler = VarphiToPythonCompiler()
|
|
86
|
+
py_code = timed_step("[2/3] Compiling Varphi IR to Python", verbose, compiler.compile, vp_source)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
typer.echo(f"Error while compiling Varphi to Python: {e}", err=True)
|
|
89
|
+
raise typer.Exit(code=1)
|
|
90
|
+
|
|
91
|
+
# Run the py
|
|
92
|
+
execution_globals = {"__name__": "__main__", "__file__": "<re2vp_execution>", "__builtins__": __builtins__}
|
|
93
|
+
original_stdin, original_stdout, original_argv = sys.stdin, sys.stdout, sys.argv
|
|
94
|
+
captured_stdout = io.StringIO()
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# We will replace stdin with our own buffer
|
|
98
|
+
# We do this so we can send in an entire payload (which may include newlines) on one tape
|
|
99
|
+
# If we don't do this, then the tape content will get cut off at the first newline
|
|
100
|
+
sys.stdin = MultiLineTapeBuffer(search_payload)
|
|
101
|
+
sys.stdout = captured_stdout
|
|
102
|
+
sys.argv = ["re2vp-run"] + (["--debug"] if debug else [])
|
|
103
|
+
|
|
104
|
+
if verbose:
|
|
105
|
+
typer.echo("[3/3] Executing Varphi Machine...", err=True)
|
|
106
|
+
t0 = time.perf_counter()
|
|
107
|
+
|
|
108
|
+
exec(py_code, execution_globals)
|
|
109
|
+
|
|
110
|
+
except SystemExit:
|
|
111
|
+
pass
|
|
112
|
+
except Exception as e:
|
|
113
|
+
typer.echo(f"Runtime Error: {e}", err=True)
|
|
114
|
+
raise typer.Exit(code=1)
|
|
115
|
+
finally:
|
|
116
|
+
sys.stdin, sys.stdout, sys.argv = original_stdin, original_stdout, original_argv
|
|
117
|
+
if verbose:
|
|
118
|
+
typer.echo(f" Done in {time.perf_counter() - t0:.4f} seconds.\n", err=True)
|
|
119
|
+
|
|
120
|
+
# Evaluate result
|
|
121
|
+
output = captured_stdout.getvalue().strip()
|
|
122
|
+
if output:
|
|
123
|
+
final_state = output.splitlines()[-1].strip()
|
|
124
|
+
typer.echo(final_state, err=True)
|
|
125
|
+
raise typer.Exit(code=0 if final_state == "ACCEPT" else 1)
|
|
126
|
+
|
|
127
|
+
typer.echo("Error: No output generated by Varphi runtime.", err=True)
|
|
128
|
+
raise typer.Exit(code=1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main():
|
|
132
|
+
app()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from greenery import parse
|
|
2
|
+
|
|
3
|
+
def compile_regex_to_varphi(regex_str: str) -> str:
|
|
4
|
+
"""Compiles a standard regular expression string into Varphi source code."""
|
|
5
|
+
# Strip ^ and $ since greenery isn't compatible with them
|
|
6
|
+
if regex_str.startswith("^"):
|
|
7
|
+
regex_str = regex_str[1:]
|
|
8
|
+
if regex_str.endswith("$"):
|
|
9
|
+
regex_str = regex_str[:-1]
|
|
10
|
+
|
|
11
|
+
# Parse the regex and compute the DFA
|
|
12
|
+
dfa = parse(regex_str).to_fsm()
|
|
13
|
+
|
|
14
|
+
lines = [
|
|
15
|
+
f"// --- Varphi Regex Machine ---",
|
|
16
|
+
f"// Pattern: {regex_str}",
|
|
17
|
+
f"// States: {len(dfa.states)}",
|
|
18
|
+
""
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# We need a clean naming convention because greenery uses complex objects/tuples for state IDs
|
|
22
|
+
normalized_states = {}
|
|
23
|
+
next_state_number = 0
|
|
24
|
+
|
|
25
|
+
# Go through the DFA's routing table
|
|
26
|
+
for current_state, paths in dfa.map.items():
|
|
27
|
+
# Register the current state if we haven't seen it yet
|
|
28
|
+
if current_state not in normalized_states:
|
|
29
|
+
normalized_states[current_state] = f"q{next_state_number}"
|
|
30
|
+
next_state_number += 1
|
|
31
|
+
|
|
32
|
+
has_wildcard = False
|
|
33
|
+
# At first, it's assumed that if no concrete rule of this state matches, we go to REJECT
|
|
34
|
+
fallback_target = "REJECT"
|
|
35
|
+
for charclass, next_state in paths.items():
|
|
36
|
+
# Register destination states dynamically as we encounter them
|
|
37
|
+
if next_state not in normalized_states:
|
|
38
|
+
normalized_states[next_state] = f"q{next_state_number}"
|
|
39
|
+
next_state_number += 1
|
|
40
|
+
|
|
41
|
+
# In greenery, wildcards/fallbacks are represented by negated character classes
|
|
42
|
+
if charclass.negated:
|
|
43
|
+
has_wildcard = True
|
|
44
|
+
# So if we match anything other than a concrete rule now, go to this state since it is now the fallback
|
|
45
|
+
fallback_target = normalized_states[next_state]
|
|
46
|
+
else:
|
|
47
|
+
# Positive character class: extract every literal character inside it
|
|
48
|
+
for char in charclass.get_chars():
|
|
49
|
+
# Instead of using the characters, just use the ascii code to avoid issues with special characters
|
|
50
|
+
safe_sym = str(ord(char))
|
|
51
|
+
lines.append(f"{normalized_states[current_state]} ({safe_sym}) {normalized_states[next_state]} ({safe_sym}) (RIGHT)")
|
|
52
|
+
|
|
53
|
+
# We need special transitions for if we hit the end of string while on this state
|
|
54
|
+
if current_state in dfa.finals:
|
|
55
|
+
# If we hit BLANK in a valid final (accepting) state, the payload is accepted
|
|
56
|
+
lines.append(f"{normalized_states[current_state]} (BLANK) ACCEPT (BLANK) (STAY)")
|
|
57
|
+
else:
|
|
58
|
+
# If we hit BLANK in a non-final state, the payload terminated too early
|
|
59
|
+
lines.append(f"{normalized_states[current_state]} (BLANK) REJECT (BLANK) (STAY)")
|
|
60
|
+
|
|
61
|
+
if has_wildcard:
|
|
62
|
+
# The DFA defined a valid path for "anything else", so use a Varphi variable to route there
|
|
63
|
+
lines.append(f"{normalized_states[current_state]} ($anythingElse) {fallback_target} ($anythingElse) (RIGHT)")
|
|
64
|
+
else:
|
|
65
|
+
# The DFA has no wildcard, so any unrecognized character is an instant failure
|
|
66
|
+
lines.append(f"{normalized_states[current_state]} ($anythingElse) REJECT ($anythingElse) (STAY)")
|
|
67
|
+
|
|
68
|
+
lines.append("") # Empty line for readability between state blocks
|
|
69
|
+
|
|
70
|
+
return "\n".join(lines)
|