pysipl 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.
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .coverage
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ *.vsix
11
+ node_modules/
pysipl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.5
2
+ Name: pysipl
3
+ Version: 0.1.0
4
+ Summary: Parser, interpreter, trace and debug adapter for the SIPL teaching language
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: lark<2,>=1.2
8
+ Provides-Extra: dev
9
+ Requires-Dist: build; extra == 'dev'
10
+ Requires-Dist: mypy; extra == 'dev'
11
+ Requires-Dist: pytest; extra == 'dev'
12
+ Requires-Dist: pytest-cov; extra == 'dev'
13
+ Requires-Dist: ruff; extra == 'dev'
@@ -0,0 +1,8 @@
1
+ begin
2
+ F := 1;
3
+ while N > 1 do
4
+ begin
5
+ F := F * N;
6
+ N := N - 1
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ begin
2
+ while ¬ (A = B) do
3
+ if A > B then
4
+ A := A - B
5
+ else
6
+ B := B - A;
7
+ G := A
8
+ end
@@ -0,0 +1,4 @@
1
+ begin
2
+ if X > 0 then
3
+ R := 1
4
+ end
@@ -0,0 +1,3 @@
1
+ begin
2
+ R := 1;
3
+ Y := Y - 1
@@ -0,0 +1,3 @@
1
+ begin
2
+ R := 1;
3
+ end
@@ -0,0 +1,8 @@
1
+ begin
2
+ R := 1;
3
+ while Y > 0 do
4
+ begin
5
+ R := R * X;
6
+ Y := Y - 1
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ begin
2
+ if X > 0 or X = 0 then
3
+ begin
4
+ if X = 0 then S := 0 else S := 1
5
+ end
6
+ else
7
+ S := -1;
8
+ skip
9
+ end
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pysipl"
7
+ dynamic = ["version"]
8
+ description = "Parser, interpreter, trace and debug adapter for the SIPL teaching language"
9
+ license = "MIT"
10
+ requires-python = ">=3.10"
11
+ dependencies = ["lark>=1.2,<2"]
12
+
13
+ [project.optional-dependencies]
14
+ dev = ["pytest", "pytest-cov", "ruff", "mypy", "build"]
15
+
16
+ [project.scripts]
17
+ sipl = "sipl.cli:main"
18
+
19
+ [tool.hatch.version]
20
+ path = "src/sipl/__init__.py"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/sipl"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
27
+
28
+ [tool.coverage.run]
29
+ source = ["sipl"]
30
+
31
+ [tool.ruff]
32
+ line-length = 100
33
+ src = ["src", "tests"]
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "F", "I", "B", "UP"]
37
+
38
+ [tool.mypy]
39
+ strict = true
40
+ files = ["src"]
@@ -0,0 +1,3 @@
1
+ """SIPL language core: parser, AST, interpreter and diagnostics."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,121 @@
1
+ """Typed AST of SIPL. Every node knows its position in the source text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, fields, is_dataclass
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class SourceSpan:
11
+ """Source range: lines and columns are 1-based, the end column is exclusive."""
12
+
13
+ start_line: int
14
+ start_column: int
15
+ end_line: int
16
+ end_column: int
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class Number:
21
+ value: int
22
+ span: SourceSpan
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class Var:
27
+ name: str
28
+ span: SourceSpan
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class BinOp:
33
+ op: str # one of + - *
34
+ left: AExpr
35
+ right: AExpr
36
+ span: SourceSpan
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class Compare:
41
+ op: str # one of = >
42
+ left: AExpr
43
+ right: AExpr
44
+ span: SourceSpan
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class Or:
49
+ left: BExpr
50
+ right: BExpr
51
+ span: SourceSpan
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class Not:
56
+ operand: BExpr
57
+ span: SourceSpan
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class Assign:
62
+ name: str
63
+ value: AExpr
64
+ span: SourceSpan
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class Seq:
69
+ """S1; S2; ... flattened: sequencing is associative, and this keeps long programs shallow."""
70
+
71
+ statements: tuple[Stmt, ...]
72
+ span: SourceSpan
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class If:
77
+ condition: BExpr
78
+ then_branch: Stmt
79
+ else_branch: Stmt
80
+ span: SourceSpan
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class While:
85
+ condition: BExpr
86
+ body: Stmt
87
+ span: SourceSpan
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class Block:
92
+ body: Stmt
93
+ span: SourceSpan
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class Skip:
98
+ span: SourceSpan
99
+
100
+
101
+ @dataclass(frozen=True, slots=True)
102
+ class Program:
103
+ body: Stmt
104
+ span: SourceSpan
105
+
106
+
107
+ AExpr = Number | Var | BinOp
108
+ BExpr = Compare | Or | Not
109
+ Expr = AExpr | BExpr
110
+ Stmt = Assign | Seq | If | While | Block | Skip
111
+ Node = Expr | Stmt | Program
112
+
113
+
114
+ def to_dict(value: Any) -> Any:
115
+ """Convert an AST node or span to plain JSON-compatible data."""
116
+ if isinstance(value, tuple):
117
+ return [to_dict(item) for item in value]
118
+ if is_dataclass(value):
119
+ data = {f.name: to_dict(getattr(value, f.name)) for f in fields(value)}
120
+ return data if isinstance(value, SourceSpan) else {"type": type(value).__name__, **data}
121
+ return value
@@ -0,0 +1,213 @@
1
+ """Command line interface: parse, check, run, trace. The JSON format is described in README."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from collections.abc import Sequence
9
+ from typing import Any
10
+
11
+ from sipl.ast import Program, to_dict
12
+ from sipl.diagnostics import Diagnostic, SiplRuntimeError, SiplSyntaxError
13
+ from sipl.interpreter import (
14
+ DEFAULT_MAX_STEPS,
15
+ AfterStatement,
16
+ BeforeStatement,
17
+ ConditionChecked,
18
+ Event,
19
+ Finished,
20
+ VariableChanged,
21
+ event_to_dict,
22
+ execute,
23
+ parse_state,
24
+ )
25
+ from sipl.parser import parse
26
+
27
+ SCHEMA_VERSION = 1
28
+ EXIT_OK = 0
29
+ EXIT_SYNTAX = 1
30
+ EXIT_USAGE = 2
31
+ EXIT_RUNTIME = 3
32
+
33
+
34
+ def _positive_int(text: str) -> int:
35
+ value = int(text)
36
+ if value <= 0:
37
+ raise argparse.ArgumentTypeError("очікувалось додатне ціле число")
38
+ return value
39
+
40
+
41
+ def _build_parser() -> argparse.ArgumentParser:
42
+ parser = argparse.ArgumentParser(prog="sipl", description="Інструменти мови SIPL")
43
+ commands = parser.add_subparsers(dest="command", required=True)
44
+ for name, description in (
45
+ ("parse", "вивести AST програми"),
46
+ ("check", "перевірити синтаксис"),
47
+ ("run", "виконати програму й вивести кінцевий стан"),
48
+ ("trace", "виконати програму й вивести всі події"),
49
+ ):
50
+ command = commands.add_parser(name, help=description, description=description)
51
+ command.add_argument("file", help="файл .sipl")
52
+ command.add_argument("--json", action="store_true", help="структурований вивід")
53
+ if name in ("run", "trace"):
54
+ command.add_argument("--input", default="", help='початковий стан, напр. "X=2 Y=10"')
55
+ command.add_argument(
56
+ "--max-steps",
57
+ type=_positive_int,
58
+ default=DEFAULT_MAX_STEPS,
59
+ help="ліміт кроків виконання (типово %(default)s)",
60
+ )
61
+ return parser
62
+
63
+
64
+ def _emit_json(command: str, ok: bool, diagnostics: list[Diagnostic], **fields: Any) -> None:
65
+ payload = {
66
+ "schema_version": SCHEMA_VERSION,
67
+ "command": command,
68
+ "ok": ok,
69
+ **fields,
70
+ "diagnostics": [diagnostic.to_dict() for diagnostic in diagnostics],
71
+ }
72
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
73
+
74
+
75
+ def _failure_fields(command: str) -> dict[str, Any]:
76
+ """Result fields of a command that stopped before producing them."""
77
+ fields: dict[str, dict[str, Any]] = {
78
+ "parse": {"ast": None},
79
+ "check": {},
80
+ "run": {"state": None, "steps": None},
81
+ "trace": {"events": [], "state": None, "steps": None},
82
+ }
83
+ return fields[command]
84
+
85
+
86
+ def _print_ast(node: Any, indent: int = 0, label: str = "") -> None:
87
+ """Print an AST tree from its JSON form, so text and JSON can never disagree."""
88
+ span = node["span"]
89
+ scalars = "".join(
90
+ f" {key}={value}"
91
+ for key, value in node.items()
92
+ if key not in ("type", "span") and not isinstance(value, (dict, list))
93
+ )
94
+ position = f"{span['start_line']}:{span['start_column']}"
95
+ print(f"{' ' * indent}{label}{node['type']}{scalars} @{position}")
96
+ for key, value in node.items():
97
+ if isinstance(value, dict) and key != "span":
98
+ _print_ast(value, indent + 1, f"{key}: ")
99
+ elif isinstance(value, list):
100
+ for item in value:
101
+ _print_ast(item, indent + 1)
102
+
103
+
104
+ def _print_state(state: dict[str, int]) -> None:
105
+ for name in sorted(state):
106
+ print(f"{name} = {state[name]}")
107
+
108
+
109
+ def _format_event(event: Event) -> str:
110
+ position = f"{event.span.start_line}:{event.span.start_column}"
111
+ match event:
112
+ case BeforeStatement(statement=statement) | AfterStatement(statement=statement):
113
+ details = statement
114
+ case VariableChanged(name=name, old=old, new=new):
115
+ details = f"{name}: {'—' if old is None else old} -> {new}"
116
+ case ConditionChecked(value=value):
117
+ details = "true" if value else "false"
118
+ case Finished(steps=steps):
119
+ details = f"кроків: {steps}"
120
+ return f"{position} {type(event).__name__} {details}"
121
+
122
+
123
+ def _execute(args: argparse.Namespace, program: Program) -> int:
124
+ try:
125
+ state = parse_state(args.input)
126
+ except ValueError as error:
127
+ return _fail(args, Diagnostic("invalid-input", str(error)), EXIT_USAGE)
128
+
129
+ tracing = args.command == "trace"
130
+ events: list[dict[str, Any]] = []
131
+ steps = None
132
+ try:
133
+ for event in execute(program, state, max_steps=args.max_steps):
134
+ if isinstance(event, Finished):
135
+ steps = event.steps
136
+ if tracing and args.json:
137
+ events.append(event_to_dict(event))
138
+ elif tracing:
139
+ print(_format_event(event))
140
+ except SiplRuntimeError as error:
141
+ return _fail(
142
+ args,
143
+ error.diagnostic,
144
+ EXIT_RUNTIME,
145
+ {"state": state, "steps": None, **({"events": events} if tracing else {})},
146
+ )
147
+
148
+ if args.json:
149
+ fields = {"state": state, "steps": steps, **({"events": events} if tracing else {})}
150
+ _emit_json(args.command, True, [], **fields)
151
+ else:
152
+ _print_state(state)
153
+ return EXIT_OK
154
+
155
+
156
+ def _fail(
157
+ args: argparse.Namespace,
158
+ diagnostic: Diagnostic,
159
+ code: int,
160
+ fields: dict[str, Any] | None = None,
161
+ ) -> int:
162
+ if args.json:
163
+ _emit_json(
164
+ args.command,
165
+ False,
166
+ [diagnostic],
167
+ **(_failure_fields(args.command) if fields is None else fields),
168
+ )
169
+ else:
170
+ print(diagnostic.format(args.file), file=sys.stderr)
171
+ return code
172
+
173
+
174
+ def main(argv: Sequence[str] | None = None) -> int:
175
+ for stream in (sys.stdout, sys.stderr):
176
+ # Pipes on Windows default to a legacy code page that cannot encode all messages.
177
+ if hasattr(stream, "reconfigure"):
178
+ stream.reconfigure(encoding="utf-8")
179
+
180
+ args = _build_parser().parse_args(argv)
181
+ try:
182
+ with open(args.file, encoding="utf-8-sig") as file:
183
+ text = file.read()
184
+ except (OSError, UnicodeDecodeError) as error:
185
+ return _fail(
186
+ args,
187
+ Diagnostic("cannot-read-file", f"Не вдалося прочитати файл: {error}"),
188
+ EXIT_USAGE,
189
+ )
190
+
191
+ try:
192
+ program = parse(text)
193
+ except SiplSyntaxError as error:
194
+ return _fail(args, error.diagnostic, EXIT_SYNTAX)
195
+
196
+ if args.command == "parse":
197
+ ast = to_dict(program)
198
+ if args.json:
199
+ _emit_json("parse", True, [], ast=ast)
200
+ else:
201
+ _print_ast(ast)
202
+ return EXIT_OK
203
+ if args.command == "check":
204
+ if args.json:
205
+ _emit_json("check", True, [])
206
+ else:
207
+ print(f"{args.file}: синтаксис коректний")
208
+ return EXIT_OK
209
+ return _execute(args, program)
210
+
211
+
212
+ if __name__ == "__main__":
213
+ sys.exit(main())