tactus 0.31.2__py3-none-any.whl
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.
- tactus/__init__.py +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.2.dist-info/METADATA +1809 -0
- tactus-0.31.2.dist-info/RECORD +160 -0
- tactus-0.31.2.dist-info/WHEEL +4 -0
- tactus-0.31.2.dist-info/entry_points.txt +2 -0
- tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safe Lua standard library implementations with determinism warnings.
|
|
3
|
+
|
|
4
|
+
Provides wrapped versions of math.random, os.time, os.date, os.clock that
|
|
5
|
+
warn when used outside checkpoint boundaries to prevent replay bugs in
|
|
6
|
+
Tactus's checkpoint-and-replay execution system.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import warnings
|
|
10
|
+
import time
|
|
11
|
+
import random
|
|
12
|
+
import math
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Callable, Any, Optional
|
|
15
|
+
from functools import wraps
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DeterminismWarning(UserWarning):
|
|
19
|
+
"""Warning issued when non-deterministic function called outside checkpoint."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NonDeterministicError(Exception):
|
|
25
|
+
"""Raised in strict mode when non-deterministic function called outside checkpoint."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def warn_if_unsafe(operation_name: str, get_context: Callable[[], Optional[Any]]):
|
|
31
|
+
"""
|
|
32
|
+
Decorator that warns when non-deterministic operation used outside checkpoint.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
operation_name: Human-readable name of operation (e.g., "math.random()")
|
|
36
|
+
get_context: Callback to retrieve current ExecutionContext
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Decorated function that checks checkpoint scope
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def decorator(func):
|
|
43
|
+
@wraps(func)
|
|
44
|
+
def wrapper(*args, **kwargs):
|
|
45
|
+
# Get context via callback
|
|
46
|
+
context = get_context()
|
|
47
|
+
|
|
48
|
+
# If no context, can't enforce (allow silently for REPL/testing)
|
|
49
|
+
if context is None:
|
|
50
|
+
return func(*args, **kwargs)
|
|
51
|
+
|
|
52
|
+
# Check if inside checkpoint
|
|
53
|
+
inside_checkpoint = getattr(context, "_inside_checkpoint", False)
|
|
54
|
+
|
|
55
|
+
if not inside_checkpoint:
|
|
56
|
+
message = (
|
|
57
|
+
f"\n{'=' * 70}\n"
|
|
58
|
+
f"DETERMINISM WARNING: {operation_name} called outside checkpoint\n"
|
|
59
|
+
f"{'=' * 70}\n\n"
|
|
60
|
+
f"Non-deterministic operations must be wrapped in checkpoints "
|
|
61
|
+
f"for durability.\n\n"
|
|
62
|
+
f"To fix, wrap your code in a checkpoint:\n\n"
|
|
63
|
+
f" -- Lua example:\n"
|
|
64
|
+
f" local random_value = Step.checkpoint(function()\n"
|
|
65
|
+
f" return {operation_name}\n"
|
|
66
|
+
f" end)\n\n"
|
|
67
|
+
f"Or use checkpoint() directly:\n\n"
|
|
68
|
+
f" local result = checkpoint(function()\n"
|
|
69
|
+
f" -- Your non-deterministic code here\n"
|
|
70
|
+
f" return {operation_name}\n"
|
|
71
|
+
f" end)\n\n"
|
|
72
|
+
f"Why: Tactus uses checkpointing for durable execution. "
|
|
73
|
+
f"Operations outside\n"
|
|
74
|
+
f"checkpoints may produce different results on replay, "
|
|
75
|
+
f"breaking determinism.\n"
|
|
76
|
+
f"\n{'=' * 70}\n"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Check strict mode
|
|
80
|
+
strict_mode = getattr(context, "strict_determinism", False)
|
|
81
|
+
|
|
82
|
+
if strict_mode:
|
|
83
|
+
raise NonDeterministicError(message)
|
|
84
|
+
else:
|
|
85
|
+
# Use stacklevel=4 to point to Lua code, not wrapper
|
|
86
|
+
warnings.warn(message, DeterminismWarning, stacklevel=4)
|
|
87
|
+
|
|
88
|
+
return func(*args, **kwargs)
|
|
89
|
+
|
|
90
|
+
return wrapper
|
|
91
|
+
|
|
92
|
+
return decorator
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def create_safe_math_library(get_context: Callable, strict_mode: bool = False):
|
|
96
|
+
"""
|
|
97
|
+
Create safe math library with warnings for random functions.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
get_context: Callback returning current ExecutionContext
|
|
101
|
+
strict_mode: If True, raise errors instead of warnings (not currently used - context has it)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dict with safe math functions (keys are strings for Lua table)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
@warn_if_unsafe("math.random()", get_context)
|
|
108
|
+
def safe_random(m=None, n=None):
|
|
109
|
+
"""
|
|
110
|
+
Safe math.random() with checkpoint warning.
|
|
111
|
+
|
|
112
|
+
Lua's math.random() has three forms:
|
|
113
|
+
- math.random(): returns float in [0, 1)
|
|
114
|
+
- math.random(n): returns integer in [1, n]
|
|
115
|
+
- math.random(m, n): returns integer in [m, n]
|
|
116
|
+
"""
|
|
117
|
+
if m is None and n is None:
|
|
118
|
+
# No arguments: return float [0, 1)
|
|
119
|
+
return random.random()
|
|
120
|
+
elif n is None:
|
|
121
|
+
# One argument: return integer [1, m]
|
|
122
|
+
return random.randint(1, int(m))
|
|
123
|
+
else:
|
|
124
|
+
# Two arguments: return integer [m, n]
|
|
125
|
+
return random.randint(int(m), int(n))
|
|
126
|
+
|
|
127
|
+
@warn_if_unsafe("math.randomseed()", get_context)
|
|
128
|
+
def safe_randomseed(seed):
|
|
129
|
+
"""Safe math.randomseed() with checkpoint warning."""
|
|
130
|
+
random.seed(int(seed))
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# Standard math functions (deterministic - pass through)
|
|
134
|
+
return {
|
|
135
|
+
# Deterministic functions - safe to use anywhere
|
|
136
|
+
"abs": abs,
|
|
137
|
+
"acos": math.acos,
|
|
138
|
+
"asin": math.asin,
|
|
139
|
+
"atan": math.atan,
|
|
140
|
+
"atan2": math.atan2,
|
|
141
|
+
"ceil": math.ceil,
|
|
142
|
+
"cos": math.cos,
|
|
143
|
+
"cosh": math.cosh,
|
|
144
|
+
"deg": math.degrees,
|
|
145
|
+
"exp": math.exp,
|
|
146
|
+
"floor": math.floor,
|
|
147
|
+
"fmod": math.fmod,
|
|
148
|
+
"huge": float("inf"),
|
|
149
|
+
"log": math.log,
|
|
150
|
+
"log10": math.log10,
|
|
151
|
+
"max": max,
|
|
152
|
+
"min": min,
|
|
153
|
+
"modf": math.modf,
|
|
154
|
+
"pi": math.pi,
|
|
155
|
+
"pow": pow,
|
|
156
|
+
"rad": math.radians,
|
|
157
|
+
"sin": math.sin,
|
|
158
|
+
"sinh": math.sinh,
|
|
159
|
+
"sqrt": math.sqrt,
|
|
160
|
+
"tan": math.tan,
|
|
161
|
+
"tanh": math.tanh,
|
|
162
|
+
# Non-deterministic functions (wrapped with warnings)
|
|
163
|
+
"random": safe_random,
|
|
164
|
+
"randomseed": safe_randomseed,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def create_safe_os_library(get_context: Callable, strict_mode: bool = False):
|
|
169
|
+
"""
|
|
170
|
+
Create safe os library with warnings for non-deterministic functions.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
get_context: Callback returning current ExecutionContext
|
|
174
|
+
strict_mode: If True, raise errors instead of warnings (not currently used - context has it)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Dict with safe os functions
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
@warn_if_unsafe("os.time()", get_context)
|
|
181
|
+
def safe_time(date_table=None):
|
|
182
|
+
"""Safe os.time() with checkpoint warning."""
|
|
183
|
+
if date_table is None:
|
|
184
|
+
return int(time.time())
|
|
185
|
+
else:
|
|
186
|
+
# Lua date table format: {year, month, day, hour, min, sec}
|
|
187
|
+
# For simplicity, just return current time
|
|
188
|
+
# Full implementation would parse the table
|
|
189
|
+
return int(time.time())
|
|
190
|
+
|
|
191
|
+
@warn_if_unsafe("os.date()", get_context)
|
|
192
|
+
def safe_date(format_str=None):
|
|
193
|
+
"""Safe os.date() with checkpoint warning."""
|
|
194
|
+
now = datetime.utcnow()
|
|
195
|
+
|
|
196
|
+
if format_str is None:
|
|
197
|
+
# Default format like Lua's os.date()
|
|
198
|
+
return now.strftime("%a %b %d %H:%M:%S %Y")
|
|
199
|
+
elif format_str == "%Y-%m-%dT%H:%M:%SZ":
|
|
200
|
+
# ISO 8601 format
|
|
201
|
+
return now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
202
|
+
else:
|
|
203
|
+
# Support Python strftime formats
|
|
204
|
+
try:
|
|
205
|
+
return now.strftime(format_str)
|
|
206
|
+
except Exception:
|
|
207
|
+
return now.strftime("%a %b %d %H:%M:%S %Y")
|
|
208
|
+
|
|
209
|
+
@warn_if_unsafe("os.clock()", get_context)
|
|
210
|
+
def safe_clock():
|
|
211
|
+
"""Safe os.clock() with checkpoint warning."""
|
|
212
|
+
return time.process_time()
|
|
213
|
+
|
|
214
|
+
@warn_if_unsafe("os.getenv()", get_context)
|
|
215
|
+
def safe_getenv(varname):
|
|
216
|
+
"""Safe os.getenv() with checkpoint warning - environment variables can change."""
|
|
217
|
+
import os
|
|
218
|
+
|
|
219
|
+
return os.getenv(varname)
|
|
220
|
+
|
|
221
|
+
@warn_if_unsafe("os.tmpname()", get_context)
|
|
222
|
+
def safe_tmpname():
|
|
223
|
+
"""Safe os.tmpname() with checkpoint warning - generates unique temporary filenames."""
|
|
224
|
+
import tempfile
|
|
225
|
+
|
|
226
|
+
return tempfile.mktemp()
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"time": safe_time,
|
|
230
|
+
"date": safe_date,
|
|
231
|
+
"clock": safe_clock,
|
|
232
|
+
"getenv": safe_getenv,
|
|
233
|
+
"tmpname": safe_tmpname,
|
|
234
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import TextIO
|
|
2
|
+
from antlr4 import *
|
|
3
|
+
from antlr4.Token import CommonToken
|
|
4
|
+
import sys
|
|
5
|
+
from typing import TextIO
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LuaLexerBase(Lexer):
|
|
9
|
+
def __init__(self, input: InputStream, output: TextIO = sys.stdout):
|
|
10
|
+
super().__init__(input, output)
|
|
11
|
+
self.start_line = 0
|
|
12
|
+
self.start_col = 0
|
|
13
|
+
|
|
14
|
+
def HandleComment(self):
|
|
15
|
+
self.start_line = self.line
|
|
16
|
+
self.start_col = self.column - 2
|
|
17
|
+
cs = self._input
|
|
18
|
+
|
|
19
|
+
if cs.LA(1) == 91: # '['
|
|
20
|
+
sep = self.skip_sep(cs)
|
|
21
|
+
if sep >= 2:
|
|
22
|
+
self.read_long_string(cs, sep)
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
while cs.LA(1) != 10 and cs.LA(1) != -1: # '\n'
|
|
26
|
+
self._interp.consume(cs)
|
|
27
|
+
|
|
28
|
+
def read_long_string(self, cs: InputStream, sep: int):
|
|
29
|
+
done = False
|
|
30
|
+
self._interp.consume(cs)
|
|
31
|
+
|
|
32
|
+
while not done:
|
|
33
|
+
c = cs.LA(1)
|
|
34
|
+
if c == -1:
|
|
35
|
+
done = True
|
|
36
|
+
elif c == 93: # ']'
|
|
37
|
+
if self.skip_sep(cs) == sep:
|
|
38
|
+
self._interp.consume(cs)
|
|
39
|
+
done = True
|
|
40
|
+
else:
|
|
41
|
+
if cs.LA(1) == -1:
|
|
42
|
+
done = True
|
|
43
|
+
else:
|
|
44
|
+
self._interp.consume(cs)
|
|
45
|
+
|
|
46
|
+
def skip_sep(self, cs: InputStream):
|
|
47
|
+
count = 0
|
|
48
|
+
s = cs.LA(1)
|
|
49
|
+
self._interp.consume(cs)
|
|
50
|
+
|
|
51
|
+
while cs.LA(1) == 61: # '='
|
|
52
|
+
self._interp.consume(cs)
|
|
53
|
+
count += 1
|
|
54
|
+
|
|
55
|
+
if cs.LA(1) == s:
|
|
56
|
+
count += 2
|
|
57
|
+
elif count == 0:
|
|
58
|
+
count = 1
|
|
59
|
+
else:
|
|
60
|
+
count = 0
|
|
61
|
+
|
|
62
|
+
return count
|
|
63
|
+
|
|
64
|
+
def IsLine1Col0(self):
|
|
65
|
+
cs = self._input
|
|
66
|
+
return cs.index == 1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from antlr4 import *
|
|
3
|
+
|
|
4
|
+
if sys.version_info[1] > 5:
|
|
5
|
+
from typing import TextIO
|
|
6
|
+
else:
|
|
7
|
+
from typing.io import TextIO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LuaParserBase(Parser):
|
|
11
|
+
debug = False
|
|
12
|
+
|
|
13
|
+
def __init__(self, input: TokenStream, output: TextIO = sys.stdout):
|
|
14
|
+
super().__init__(input, output)
|
|
15
|
+
|
|
16
|
+
def IsFunctionCall(self) -> bool:
|
|
17
|
+
la = self._input.LT(1)
|
|
18
|
+
if la.type != self.NAME:
|
|
19
|
+
return False
|
|
20
|
+
la = self._input.LT(2)
|
|
21
|
+
if la.type == self.OP:
|
|
22
|
+
return False
|
|
23
|
+
return True
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Tactus DSL Validation
|
|
2
|
+
|
|
3
|
+
This module provides ANTLR-based validation for `.tac` files.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
The validation system uses a three-phase approach:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
.tac file
|
|
11
|
+
↓
|
|
12
|
+
Phase 1: ANTLR Parsing (Lua 5.4 grammar)
|
|
13
|
+
↓
|
|
14
|
+
Parse Tree
|
|
15
|
+
↓
|
|
16
|
+
Phase 2: Semantic Visitor (DSL pattern recognition)
|
|
17
|
+
↓
|
|
18
|
+
Registry with Declarations
|
|
19
|
+
↓
|
|
20
|
+
Phase 3: Registry Validation (cross-reference checking)
|
|
21
|
+
↓
|
|
22
|
+
ValidationResult
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Key Components
|
|
26
|
+
|
|
27
|
+
1. **ANTLR Grammar** (`grammar/LuaLexer.g4`, `grammar/LuaParser.g4`)
|
|
28
|
+
- Standard Lua 5.4 grammar from antlr/grammars-v4
|
|
29
|
+
- Unmodified - semantic layer added on top
|
|
30
|
+
- Same grammar generates both Python and TypeScript parsers
|
|
31
|
+
|
|
32
|
+
2. **Generated Parsers** (`generated/`)
|
|
33
|
+
- `LuaLexer.py` / `LuaParser.py` - Python parser
|
|
34
|
+
- `LuaLexer.ts` / `LuaParser.ts` - TypeScript parser
|
|
35
|
+
- Generated from grammar using ANTLR4
|
|
36
|
+
- Committed to version control
|
|
37
|
+
|
|
38
|
+
3. **Semantic Visitor** (`semantic_visitor.py`)
|
|
39
|
+
- Walks ANTLR parse tree
|
|
40
|
+
- Recognizes DSL function calls (name, parameter, agent, etc.)
|
|
41
|
+
- Extracts arguments and builds registry
|
|
42
|
+
- Does NOT execute code
|
|
43
|
+
|
|
44
|
+
4. **Validator** (`validator.py`)
|
|
45
|
+
- Main entry point for validation
|
|
46
|
+
- Coordinates parsing, semantic analysis, and registry validation
|
|
47
|
+
- Supports quick (syntax only) and full (semantic) modes
|
|
48
|
+
|
|
49
|
+
## Validation Modes
|
|
50
|
+
|
|
51
|
+
### Quick Mode
|
|
52
|
+
- ANTLR parse only
|
|
53
|
+
- Catches syntax errors
|
|
54
|
+
- Fast (suitable for IDE real-time validation)
|
|
55
|
+
- Returns immediately after syntax check
|
|
56
|
+
|
|
57
|
+
### Full Mode
|
|
58
|
+
- ANTLR parse + semantic analysis + registry validation
|
|
59
|
+
- Catches syntax errors, DSL errors, and semantic errors
|
|
60
|
+
- Slower but thorough
|
|
61
|
+
- Used by CLI and pre-execution validation
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Python
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from tactus.validation import TactusValidator, ValidationMode
|
|
69
|
+
|
|
70
|
+
validator = TactusValidator()
|
|
71
|
+
|
|
72
|
+
# Quick validation (syntax only)
|
|
73
|
+
result = validator.validate(source, ValidationMode.QUICK)
|
|
74
|
+
|
|
75
|
+
# Full validation (syntax + semantics)
|
|
76
|
+
result = validator.validate(source, ValidationMode.FULL)
|
|
77
|
+
|
|
78
|
+
if result.valid:
|
|
79
|
+
print(f"Valid! Procedure: {result.registry.procedure_name}")
|
|
80
|
+
else:
|
|
81
|
+
for error in result.errors:
|
|
82
|
+
print(f"Error at {error.location}: {error.message}")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### CLI
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Validate a file
|
|
89
|
+
tactus validate examples/01-basics-hello-world.tac
|
|
90
|
+
|
|
91
|
+
# Quick validation (syntax only)
|
|
92
|
+
tactus validate examples/01-basics-hello-world.tac --quick
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Parser Generation
|
|
96
|
+
|
|
97
|
+
### Requirements
|
|
98
|
+
|
|
99
|
+
**Docker is REQUIRED** for parser generation:
|
|
100
|
+
- ANTLR4 requires Java Runtime
|
|
101
|
+
- We use Docker to avoid Java installation
|
|
102
|
+
- Image: `eclipse-temurin:17-jre`
|
|
103
|
+
|
|
104
|
+
### Regenerating Parsers
|
|
105
|
+
|
|
106
|
+
**When to regenerate:**
|
|
107
|
+
- Only when modifying grammar files
|
|
108
|
+
- Generated parsers are committed to repo
|
|
109
|
+
- End users don't need to regenerate
|
|
110
|
+
|
|
111
|
+
**How to regenerate:**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Ensure Docker is running
|
|
115
|
+
make generate-parsers
|
|
116
|
+
|
|
117
|
+
# Or individually:
|
|
118
|
+
make generate-python-parser
|
|
119
|
+
make generate-typescript-parser
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Manual Generation
|
|
123
|
+
|
|
124
|
+
**Python:**
|
|
125
|
+
```bash
|
|
126
|
+
docker run --rm \
|
|
127
|
+
-v $(pwd):/work \
|
|
128
|
+
-v /tmp:/tmp \
|
|
129
|
+
-w /work \
|
|
130
|
+
eclipse-temurin:17-jre \
|
|
131
|
+
java -jar /tmp/antlr-4.13.1-complete.jar \
|
|
132
|
+
-Dlanguage=Python3 \
|
|
133
|
+
-visitor \
|
|
134
|
+
-no-listener \
|
|
135
|
+
-o /work/tactus/validation/generated \
|
|
136
|
+
/work/tactus/validation/grammar/LuaLexer.g4 \
|
|
137
|
+
/work/tactus/validation/grammar/LuaParser.g4
|
|
138
|
+
|
|
139
|
+
# Fix 'this' references (ANTLR bug)
|
|
140
|
+
sed -i 's/this\./self./g' tactus/validation/generated/LuaParser.py
|
|
141
|
+
sed -i 's/this\./self./g' tactus/validation/generated/LuaLexer.py
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**TypeScript:**
|
|
145
|
+
```bash
|
|
146
|
+
docker run --rm \
|
|
147
|
+
-v $(pwd):/work \
|
|
148
|
+
-w /work/tactus-web \
|
|
149
|
+
eclipse-temurin:17-jre \
|
|
150
|
+
bash -c "
|
|
151
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - &&
|
|
152
|
+
apt-get install -y nodejs &&
|
|
153
|
+
npm install &&
|
|
154
|
+
npx antlr4ts -visitor -no-listener \
|
|
155
|
+
-o src/validation/generated \
|
|
156
|
+
/work/tactus/validation/grammar/LuaLexer.g4 \
|
|
157
|
+
/work/tactus/validation/grammar/LuaParser.g4
|
|
158
|
+
"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Testing
|
|
162
|
+
|
|
163
|
+
### Python Tests
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pytest tests/validation/test_antlr_parser.py -v
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Tests cover:
|
|
170
|
+
- Valid/invalid Lua syntax
|
|
171
|
+
- Error location reporting
|
|
172
|
+
- DSL function recognition
|
|
173
|
+
- Parameter/output/agent extraction
|
|
174
|
+
- All example files
|
|
175
|
+
- Missing required fields
|
|
176
|
+
- Quick vs full mode
|
|
177
|
+
|
|
178
|
+
### TypeScript Tests
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
cd tactus-web && npm test
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Note:** TypeScript parser currently has compilation issues due to antlr4ts code generation bugs. This is being addressed.
|
|
185
|
+
|
|
186
|
+
## Known Issues
|
|
187
|
+
|
|
188
|
+
### TypeScript Parser
|
|
189
|
+
The antlr4ts code generator has known issues:
|
|
190
|
+
- Generates code with TypeScript compilation errors
|
|
191
|
+
- Base class imports need manual fixes
|
|
192
|
+
- Some generated code references don't match antlr4ts API
|
|
193
|
+
|
|
194
|
+
**Workaround:** Python parser is fully functional and used for all validation. TypeScript parser is in progress.
|
|
195
|
+
|
|
196
|
+
## Why ANTLR?
|
|
197
|
+
|
|
198
|
+
1. **Formal Grammar** - Standard Lua 5.4 grammar, not custom
|
|
199
|
+
2. **Multi-Language** - Same grammar generates Python and TypeScript parsers
|
|
200
|
+
3. **No Execution** - Validates without running code
|
|
201
|
+
4. **IDE Support** - Parse tree enables syntax highlighting, autocomplete, etc.
|
|
202
|
+
5. **Accurate** - Catches all Lua syntax errors
|
|
203
|
+
6. **Maintainable** - Grammar is separate from implementation
|
|
204
|
+
|
|
205
|
+
## Separation of Concerns
|
|
206
|
+
|
|
207
|
+
- **Validation (ANTLR)**: Parse tree analysis, no execution
|
|
208
|
+
- **Runtime (lupa)**: Actual Lua execution with primitives
|
|
209
|
+
|
|
210
|
+
The same `.tac` file works for both:
|
|
211
|
+
- ANTLR validates structure
|
|
212
|
+
- lupa executes the procedure
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ANTLR error listener for collecting syntax errors.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from antlr4.error.ErrorListener import ErrorListener
|
|
6
|
+
from tactus.core.registry import ValidationMessage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TactusErrorListener(ErrorListener):
|
|
10
|
+
"""Collects syntax errors from ANTLR parser."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.errors = []
|
|
14
|
+
|
|
15
|
+
def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
|
|
16
|
+
"""Called when parser encounters a syntax error."""
|
|
17
|
+
self.errors.append(
|
|
18
|
+
ValidationMessage(
|
|
19
|
+
level="error", message=f"Syntax error: {msg}", location=(line, column)
|
|
20
|
+
)
|
|
21
|
+
)
|