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 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,7 @@
1
+ from .utils import compile_regex_to_varphi
2
+
3
+ __version__ = "v1.0.0"
4
+
5
+ __all__ = [
6
+ "compile_regex_to_varphi"
7
+ ]
@@ -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)