tactus 0.34.0__py3-none-any.whl → 0.35.0__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.
Files changed (81) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/broker_log.py +17 -14
  3. tactus/adapters/channels/__init__.py +17 -15
  4. tactus/adapters/channels/base.py +16 -7
  5. tactus/adapters/channels/broker.py +43 -13
  6. tactus/adapters/channels/cli.py +19 -15
  7. tactus/adapters/channels/host.py +15 -6
  8. tactus/adapters/channels/ipc.py +82 -31
  9. tactus/adapters/channels/sse.py +41 -23
  10. tactus/adapters/cli_hitl.py +19 -19
  11. tactus/adapters/cli_log.py +4 -4
  12. tactus/adapters/control_loop.py +138 -99
  13. tactus/adapters/cost_collector_log.py +9 -9
  14. tactus/adapters/file_storage.py +56 -52
  15. tactus/adapters/http_callback_log.py +23 -13
  16. tactus/adapters/ide_log.py +17 -9
  17. tactus/adapters/lua_tools.py +4 -5
  18. tactus/adapters/mcp.py +16 -19
  19. tactus/adapters/mcp_manager.py +46 -30
  20. tactus/adapters/memory.py +9 -9
  21. tactus/adapters/plugins.py +42 -42
  22. tactus/broker/client.py +75 -78
  23. tactus/broker/protocol.py +57 -57
  24. tactus/broker/server.py +252 -197
  25. tactus/cli/app.py +3 -1
  26. tactus/cli/control.py +2 -2
  27. tactus/core/config_manager.py +181 -135
  28. tactus/core/dependencies/registry.py +66 -48
  29. tactus/core/dsl_stubs.py +222 -163
  30. tactus/core/exceptions.py +10 -1
  31. tactus/core/execution_context.py +152 -112
  32. tactus/core/lua_sandbox.py +72 -64
  33. tactus/core/message_history_manager.py +138 -43
  34. tactus/core/mocking.py +41 -27
  35. tactus/core/output_validator.py +49 -44
  36. tactus/core/registry.py +94 -80
  37. tactus/core/runtime.py +211 -176
  38. tactus/core/template_resolver.py +16 -16
  39. tactus/core/yaml_parser.py +55 -45
  40. tactus/docs/extractor.py +7 -6
  41. tactus/ide/server.py +119 -78
  42. tactus/primitives/control.py +10 -6
  43. tactus/primitives/file.py +48 -46
  44. tactus/primitives/handles.py +47 -35
  45. tactus/primitives/host.py +29 -27
  46. tactus/primitives/human.py +154 -137
  47. tactus/primitives/json.py +22 -23
  48. tactus/primitives/log.py +26 -26
  49. tactus/primitives/message_history.py +285 -31
  50. tactus/primitives/model.py +15 -9
  51. tactus/primitives/procedure.py +86 -64
  52. tactus/primitives/procedure_callable.py +58 -51
  53. tactus/primitives/retry.py +31 -29
  54. tactus/primitives/session.py +42 -29
  55. tactus/primitives/state.py +54 -43
  56. tactus/primitives/step.py +9 -13
  57. tactus/primitives/system.py +34 -21
  58. tactus/primitives/tool.py +44 -31
  59. tactus/primitives/tool_handle.py +76 -54
  60. tactus/primitives/toolset.py +25 -22
  61. tactus/sandbox/config.py +4 -4
  62. tactus/sandbox/container_runner.py +161 -107
  63. tactus/sandbox/docker_manager.py +20 -20
  64. tactus/sandbox/entrypoint.py +16 -14
  65. tactus/sandbox/protocol.py +15 -15
  66. tactus/stdlib/classify/llm.py +1 -3
  67. tactus/stdlib/core/validation.py +0 -3
  68. tactus/testing/pydantic_eval_runner.py +1 -1
  69. tactus/utils/asyncio_helpers.py +27 -0
  70. tactus/utils/cost_calculator.py +7 -7
  71. tactus/utils/model_pricing.py +11 -12
  72. tactus/utils/safe_file_library.py +156 -132
  73. tactus/utils/safe_libraries.py +27 -27
  74. tactus/validation/error_listener.py +18 -5
  75. tactus/validation/semantic_visitor.py +392 -333
  76. tactus/validation/validator.py +89 -49
  77. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,6 +6,7 @@ extracting declarations without executing code.
6
6
  """
7
7
 
8
8
  import logging
9
+ import re
9
10
  from typing import Any, Optional
10
11
 
11
12
  from .generated.LuaParser import LuaParser
@@ -51,202 +52,207 @@ class TactusDSLVisitor(LuaParserVisitor):
51
52
 
52
53
  def __init__(self):
53
54
  self.builder = RegistryBuilder()
54
- self.errors = []
55
- self.warnings = []
55
+ self.errors: list[ValidationMessage] = []
56
+ self.warnings: list[ValidationMessage] = []
56
57
  self.current_line = 0
57
58
  self.current_col = 0
58
59
  self.in_function_body = False # Track if we're inside a function body
59
60
 
60
- def visitFunctiondef(self, ctx):
61
+ def _record_error(
62
+ self,
63
+ message: str,
64
+ declaration: Optional[str] = None,
65
+ line_number: Optional[int] = None,
66
+ column_number: Optional[int] = None,
67
+ ) -> None:
68
+ """Record a validation error with the current location by default."""
69
+ self.errors.append(
70
+ ValidationMessage(
71
+ level="error",
72
+ message=message,
73
+ location=(
74
+ self.current_line if line_number is None else line_number,
75
+ self.current_col if column_number is None else column_number,
76
+ ),
77
+ declaration=declaration,
78
+ )
79
+ )
80
+
81
+ def visitFunctiondef(self, context):
61
82
  """Track when entering/exiting function definitions."""
62
83
  # Set flag when entering function body
63
- old_in_function = self.in_function_body
84
+ previous_in_function_body = self.in_function_body
64
85
  self.in_function_body = True
65
86
  try:
66
- result = super().visitChildren(ctx)
87
+ child_visit_result = super().visitChildren(context)
67
88
  finally:
68
89
  # Restore previous state when exiting
69
- self.in_function_body = old_in_function
70
- return result
90
+ self.in_function_body = previous_in_function_body
91
+ return child_visit_result
71
92
 
72
- def visitStat(self, ctx: LuaParser.StatContext):
93
+ def visitStat(self, context: LuaParser.StatContext):
73
94
  """Handle statement nodes including assignments."""
74
95
  # Check if this is an assignment statement
75
- if ctx.varlist() and ctx.explist():
96
+ if context.varlist() and context.explist():
76
97
  # This is an assignment: varlist '=' explist
77
- varlist = ctx.varlist()
78
- explist = ctx.explist()
98
+ variable_list = context.varlist()
99
+ expression_list = context.explist()
79
100
 
80
101
  # Get the variable name
81
- if varlist.var() and len(varlist.var()) > 0:
82
- var = varlist.var()[0]
83
- if var.NAME():
84
- var_name = var.NAME().getText()
102
+ if variable_list.var() and len(variable_list.var()) > 0:
103
+ assignment_target_node = variable_list.var()[0]
104
+ if assignment_target_node.NAME():
105
+ assignment_target_name = assignment_target_node.NAME().getText()
85
106
 
86
107
  # Check if this is a DSL setting assignment
87
- if var_name in [
88
- "default_provider",
89
- "default_model",
90
- "return_prompt",
91
- "error_prompt",
92
- "status_prompt",
93
- "async",
94
- "max_depth",
95
- "max_turns",
96
- ]:
108
+ setting_handlers_by_name = {
109
+ "default_provider": self.builder.set_default_provider,
110
+ "default_model": self.builder.set_default_model,
111
+ "return_prompt": self.builder.set_return_prompt,
112
+ "error_prompt": self.builder.set_error_prompt,
113
+ "status_prompt": self.builder.set_status_prompt,
114
+ "async": self.builder.set_async,
115
+ "max_depth": self.builder.set_max_depth,
116
+ "max_turns": self.builder.set_max_turns,
117
+ }
118
+ if assignment_target_name in setting_handlers_by_name:
97
119
  # Get the value from explist
98
- if explist.exp() and len(explist.exp()) > 0:
99
- exp = explist.exp()[0]
100
- value = self._extract_literal_value(exp)
101
-
120
+ if expression_list.exp() and len(expression_list.exp()) > 0:
121
+ first_expression = expression_list.exp()[0]
122
+ literal_value = self._extract_literal_value(first_expression)
102
123
  # Process the assignment like a function call
103
- if var_name == "default_provider":
104
- self.builder.set_default_provider(value)
105
- elif var_name == "default_model":
106
- self.builder.set_default_model(value)
107
- elif var_name == "return_prompt":
108
- self.builder.set_return_prompt(value)
109
- elif var_name == "error_prompt":
110
- self.builder.set_error_prompt(value)
111
- elif var_name == "status_prompt":
112
- self.builder.set_status_prompt(value)
113
- elif var_name == "async":
114
- self.builder.set_async(value)
115
- elif var_name == "max_depth":
116
- self.builder.set_max_depth(value)
117
- elif var_name == "max_turns":
118
- self.builder.set_max_turns(value)
124
+ setting_handlers_by_name[assignment_target_name](literal_value)
119
125
  else:
120
126
  # Check for assignment-based DSL declarations
121
127
  # e.g., greeter = Agent {...}, done = Tool {...}
122
- if explist.exp() and len(explist.exp()) > 0:
123
- exp = explist.exp()[0]
124
- self._check_assignment_based_declaration(var_name, exp)
128
+ if expression_list.exp() and len(expression_list.exp()) > 0:
129
+ first_expression = expression_list.exp()[0]
130
+ self._check_assignment_based_declaration(
131
+ assignment_target_name, first_expression
132
+ )
125
133
 
126
134
  # Continue visiting children
127
- return self.visitChildren(ctx)
135
+ return self.visitChildren(context)
128
136
 
129
- def _check_assignment_based_declaration(self, var_name: str, exp):
137
+ def _check_assignment_based_declaration(
138
+ self, assignment_target_name: str, assignment_expression
139
+ ):
130
140
  """Check if an assignment is a DSL declaration like 'greeter = Agent {...}'."""
131
141
  # Look for prefixexp with functioncall pattern: Agent {...}
132
- if exp.prefixexp():
133
- prefixexp = exp.prefixexp()
134
- if prefixexp.functioncall():
135
- func_call = prefixexp.functioncall()
136
- func_name = self._extract_function_name(func_call)
142
+ if assignment_expression.prefixexp():
143
+ prefix_expression = assignment_expression.prefixexp()
144
+ if prefix_expression.functioncall():
145
+ function_call = prefix_expression.functioncall()
146
+ function_name = self._extract_function_name(function_call)
137
147
 
138
148
  # Check if this is a chained method call (e.g., Agent('name').turn())
139
149
  # Chained calls have structure: func_name args . method_name args
140
150
  # Simple declarations have: func_name args or func_name table
141
151
  # If there are more than 2 children, it's a chained call, not a declaration
142
- is_chained_call = func_call.getChildCount() > 2
152
+ is_chained_method_call = function_call.getChildCount() > 2
143
153
 
144
- if func_name == "Agent" and not is_chained_call:
154
+ if function_name == "Agent" and not is_chained_method_call:
145
155
  # Extract config from Agent {...}
146
- config = self._extract_single_table_arg(func_call)
156
+ declaration_config = self._extract_single_table_arg(function_call)
147
157
  # Filter out None values from tools list (variable refs can't be resolved)
148
- if config and "tools" in config:
149
- tools = config["tools"]
150
- if isinstance(tools, list):
151
- config["tools"] = [t for t in tools if t is not None]
152
- self.builder.register_agent(var_name, config if config else {}, None)
153
- elif func_name == "Tool":
158
+ if declaration_config and "tools" in declaration_config:
159
+ tool_name_list = declaration_config["tools"]
160
+ if isinstance(tool_name_list, list):
161
+ declaration_config["tools"] = [
162
+ tool_name for tool_name in tool_name_list if tool_name is not None
163
+ ]
164
+ self.builder.register_agent(
165
+ assignment_target_name,
166
+ declaration_config if declaration_config else {},
167
+ None,
168
+ )
169
+ elif function_name == "Tool":
154
170
  # Extract config from Tool {...}
155
- config = self._extract_single_table_arg(func_call)
171
+ declaration_config = self._extract_single_table_arg(function_call)
156
172
  if (
157
- config
158
- and isinstance(config, dict)
159
- and isinstance(config.get("name"), str)
160
- and config.get("name") != var_name
173
+ declaration_config
174
+ and isinstance(declaration_config, dict)
175
+ and isinstance(declaration_config.get("name"), str)
176
+ and declaration_config.get("name") != assignment_target_name
161
177
  ):
162
- self.errors.append(
163
- ValidationMessage(
164
- level="error",
165
- message=(
166
- f"Tool name mismatch: '{var_name} = Tool {{ name = \"{config.get('name')}\" }}'. "
167
- f"Remove the 'name' field or set it to '{var_name}'."
168
- ),
169
- location=(self.current_line, self.current_col),
170
- declaration="Tool",
171
- )
178
+ self._record_error(
179
+ message=(
180
+ f"Tool name mismatch: '{assignment_target_name} = Tool {{ name = \"{declaration_config.get('name')}\" }}'. "
181
+ f"Remove the 'name' field or set it to '{assignment_target_name}'."
182
+ ),
183
+ declaration="Tool",
172
184
  )
173
- self.builder.register_tool(var_name, config if config else {}, None)
174
- elif func_name == "Toolset":
185
+ self.builder.register_tool(
186
+ assignment_target_name,
187
+ declaration_config if declaration_config else {},
188
+ None,
189
+ )
190
+ elif function_name == "Toolset":
175
191
  # Extract config from Toolset {...}
176
- config = self._extract_single_table_arg(func_call)
177
- self.builder.register_toolset(var_name, config if config else {})
178
- elif func_name == "Procedure":
192
+ declaration_config = self._extract_single_table_arg(function_call)
193
+ self.builder.register_toolset(
194
+ assignment_target_name,
195
+ declaration_config if declaration_config else {},
196
+ )
197
+ elif function_name == "Procedure":
179
198
  # New assignment syntax: main = Procedure { function(input) ... }
180
199
  # Register as a named procedure
181
200
  self.builder.register_named_procedure(
182
- var_name,
201
+ assignment_target_name,
183
202
  None, # Function not available during validation
184
203
  {}, # Input schema will be extracted from top-level input {}
185
204
  {}, # Output schema will be extracted from top-level output {}
186
205
  {}, # State schema
187
206
  )
188
207
 
189
- def _extract_single_table_arg(self, func_call) -> dict:
208
+ def _extract_single_table_arg(self, function_call) -> dict:
190
209
  """Extract a single table argument from a function call like Agent {...}."""
191
- args_list = func_call.args()
192
- if not args_list:
210
+ argument_list_nodes = function_call.args()
211
+ if not argument_list_nodes:
193
212
  return {}
194
213
 
195
214
  # Process first args entry only
196
- if len(args_list) > 0:
197
- args_ctx = args_list[0]
198
- if args_ctx.tableconstructor():
199
- return self._parse_table_constructor(args_ctx.tableconstructor())
215
+ argument_context = argument_list_nodes[0]
216
+ if argument_context.tableconstructor():
217
+ return self._parse_table_constructor(argument_context.tableconstructor())
200
218
 
201
219
  return {}
202
220
 
203
- def visitFunctioncall(self, ctx: LuaParser.FunctioncallContext):
221
+ def visitFunctioncall(self, context: LuaParser.FunctioncallContext):
204
222
  """Recognize and process DSL function calls."""
205
223
  try:
206
224
  # Extract line/column for error reporting
207
- if ctx.start:
208
- self.current_line = ctx.start.line
209
- self.current_col = ctx.start.column
225
+ if context.start:
226
+ self.current_line = context.start.line
227
+ self.current_col = context.start.column
210
228
 
211
229
  # Check for deprecated method calls like .turn() or .run()
212
- self._check_deprecated_method_calls(ctx)
230
+ self._check_deprecated_method_calls(context)
213
231
 
214
- func_name = self._extract_function_name(ctx)
232
+ function_name = self._extract_function_name(context)
215
233
 
216
234
  # Check if this is a method call (e.g., Tool.called()) vs a direct call (e.g., Tool())
217
235
  # For "Tool.called()", parser extracts "Tool" as func_name but full text is "Tool.called(...)"
218
236
  # We want to skip if full_text shows it's actually calling a method ON Tool, not Tool itself
219
- full_text = ctx.getText()
220
- is_method_call = False
221
- if func_name:
222
- # If the text is "Tool.called(...)" and func_name is "Tool",
223
- # then it's actually calling .called() method on Tool, not calling Tool()
224
- # Check: does full_text have func_name followed by a dot/colon (not by opening paren)?
225
- # Pattern: func_name followed by . or : means it's accessing a method/property
226
- import re
227
-
228
- # Match: funcName followed by . or : (not by opening paren directly)
229
- method_access_pattern = re.escape(func_name) + r"[.:]"
230
- if re.search(method_access_pattern, full_text):
231
- is_method_call = True
232
-
233
- if func_name in self.DSL_FUNCTIONS and not is_method_call:
237
+ is_method_access_call = self._is_method_access_call(function_name, context)
238
+
239
+ if function_name in self.DSL_FUNCTIONS and not is_method_access_call:
234
240
  # Process the DSL call (but skip method calls like Tool.called())
235
241
  try:
236
- self._process_dsl_call(func_name, ctx)
237
- except Exception as e:
242
+ self._process_dsl_call(function_name, context)
243
+ except Exception as processing_exception:
238
244
  self.errors.append(
239
245
  ValidationMessage(
240
246
  level="error",
241
- message=f"Error processing {func_name}: {e}",
247
+ message=(f"Error processing {function_name}: {processing_exception}"),
242
248
  location=(self.current_line, self.current_col),
243
- declaration=func_name,
249
+ declaration=function_name,
244
250
  )
245
251
  )
246
- except Exception as e:
247
- logger.debug(f"Error in visitFunctioncall: {e}")
252
+ except Exception as visit_exception:
253
+ logger.debug("Error in visitFunctioncall: %s", visit_exception)
248
254
 
249
- return self.visitChildren(ctx)
255
+ return self.visitChildren(context)
250
256
 
251
257
  def _check_deprecated_method_calls(self, ctx: LuaParser.FunctioncallContext):
252
258
  """Check for deprecated method calls like .turn() or .run()."""
@@ -259,13 +265,12 @@ class TactusDSLVisitor(LuaParserVisitor):
259
265
 
260
266
  # Check for .turn() pattern (method call with dot notation)
261
267
  if ".turn(" in full_text or ":turn(" in full_text:
262
- self.errors.append(
263
- ValidationMessage(
264
- level="error",
265
- message='The .turn() method is deprecated. Use callable syntax instead: agent() or agent({message = "..."})',
266
- location=(self.current_line, self.current_col),
267
- declaration="Agent.turn",
268
- )
268
+ self._record_error(
269
+ message=(
270
+ "The .turn() method is deprecated. Use callable syntax instead: "
271
+ 'agent() or agent({message = "..."})'
272
+ ),
273
+ declaration="Agent.turn",
269
274
  )
270
275
 
271
276
  # Check for .run() pattern on agents
@@ -273,75 +278,92 @@ class TactusDSLVisitor(LuaParserVisitor):
273
278
  # Try to determine if this is an agent call (not procedure or other types)
274
279
  # If the text contains "Agent(" it's likely an agent method
275
280
  if "Agent(" in full_text or ctx.getText().startswith("agent"):
276
- self.errors.append(
277
- ValidationMessage(
278
- level="error",
279
- message='The .run() method on agents is deprecated. Use callable syntax instead: agent() or agent({message = "..."})',
280
- location=(self.current_line, self.current_col),
281
- declaration="Agent.run",
282
- )
281
+ self._record_error(
282
+ message=(
283
+ "The .run() method on agents is deprecated. Use callable syntax instead: "
284
+ 'agent() or agent({message = "..."})'
285
+ ),
286
+ declaration="Agent.run",
283
287
  )
284
288
 
285
- def _extract_literal_value(self, exp):
289
+ def _is_method_access_call(
290
+ self, function_name: Optional[str], ctx: LuaParser.FunctioncallContext
291
+ ) -> bool:
292
+ """Return True when the call is actually a method access like Tool.called()."""
293
+ if not function_name:
294
+ return False
295
+
296
+ function_call_text = ctx.getText()
297
+ method_access_pattern = re.escape(function_name) + r"[.:]"
298
+ return re.search(method_access_pattern, function_call_text) is not None
299
+
300
+ def _extract_literal_value(self, expression_node):
286
301
  """Extract a literal value from an expression node."""
287
- if not exp:
302
+ if not expression_node:
288
303
  return None
289
304
 
305
+ expression_text = expression_node.getText()
306
+
290
307
  # Check for string literals
291
- if exp.string():
292
- string_ctx = exp.string()
308
+ if expression_node.string():
309
+ string_context = expression_node.string()
293
310
  # Extract the string value (remove quotes)
294
- if string_ctx.NORMALSTRING():
295
- text = string_ctx.NORMALSTRING().getText()
296
- # Remove surrounding quotes
297
- if text.startswith('"') and text.endswith('"'):
298
- return text[1:-1]
299
- elif text.startswith("'") and text.endswith("'"):
300
- return text[1:-1]
301
- elif string_ctx.CHARSTRING():
302
- text = string_ctx.CHARSTRING().getText()
311
+ if string_context.NORMALSTRING():
312
+ string_text = string_context.NORMALSTRING().getText()
303
313
  # Remove surrounding quotes
304
- if text.startswith('"') and text.endswith('"'):
305
- return text[1:-1]
306
- elif text.startswith("'") and text.endswith("'"):
307
- return text[1:-1]
314
+ if string_text.startswith('"') and string_text.endswith('"'):
315
+ return string_text[1:-1]
316
+ elif string_text.startswith("'") and string_text.endswith("'"):
317
+ return string_text[1:-1]
318
+ elif string_context.CHARSTRING():
319
+ string_text = string_context.CHARSTRING().getText()
320
+ # Character strings are always quoted in Lua tokens.
321
+ if (
322
+ len(string_text) >= 2
323
+ and string_text[0] == string_text[-1]
324
+ and string_text[0] in ("'", '"')
325
+ ):
326
+ return string_text[1:-1]
327
+ return string_text
308
328
 
309
329
  # Check for number literals
310
- if exp.number():
311
- number_ctx = exp.number()
312
- if number_ctx.INT():
313
- return int(number_ctx.INT().getText())
314
- elif number_ctx.FLOAT():
315
- return float(number_ctx.FLOAT().getText())
330
+ if expression_node.number():
331
+ number_context = expression_node.number()
332
+ if number_context.INT():
333
+ return int(number_context.INT().getText())
334
+ elif number_context.FLOAT():
335
+ return float(number_context.FLOAT().getText())
316
336
 
317
337
  # Check for boolean literals
318
- if exp.getText() == "true":
338
+ if expression_text == "true":
319
339
  return True
320
- elif exp.getText() == "false":
340
+ elif expression_text == "false":
321
341
  return False
322
342
 
323
343
  # Check for nil
324
- if exp.getText() == "nil":
344
+ if expression_text == "nil":
325
345
  return None
326
346
 
327
347
  # Default to the text representation
328
- return exp.getText()
348
+ return expression_text
329
349
 
330
- def _extract_function_name(self, ctx: LuaParser.FunctioncallContext) -> Optional[str]:
350
+ def _extract_function_name(
351
+ self, function_call_context: LuaParser.FunctioncallContext
352
+ ) -> Optional[str]:
331
353
  """Extract function name from parse tree."""
332
354
  # The function name is the first child of functioncall
333
355
  # Look for a terminal node with text
334
- for i in range(ctx.getChildCount()):
335
- child = ctx.getChild(i)
356
+ for i in range(function_call_context.getChildCount()):
357
+ child = function_call_context.getChild(i)
336
358
  if hasattr(child, "symbol"):
337
359
  # It's a terminal node
338
- text = child.getText()
339
- if text and text.isidentifier():
340
- return text
360
+ token_text = child.getText()
361
+ if token_text and token_text.isidentifier():
362
+ return token_text
341
363
 
342
364
  # Fallback: try varOrExp approach
343
- if ctx.varOrExp():
344
- var_or_exp = ctx.varOrExp()
365
+ if function_call_context.varOrExp():
366
+ var_or_exp = function_call_context.varOrExp()
345
367
  # varOrExp: var | '(' exp ')'
346
368
  if var_or_exp.var():
347
369
  var_ctx = var_or_exp.var()
@@ -351,87 +373,96 @@ class TactusDSLVisitor(LuaParserVisitor):
351
373
 
352
374
  return None
353
375
 
354
- def _process_dsl_call(self, func_name: str, ctx: LuaParser.FunctioncallContext):
376
+ def _process_dsl_call(self, function_name: str, ctx: LuaParser.FunctioncallContext):
355
377
  """Extract arguments and register declaration."""
356
- args = self._extract_arguments(ctx)
357
-
358
- if func_name == "name":
359
- if args and len(args) >= 1:
360
- self.builder.set_name(args[0])
361
- elif func_name == "version":
362
- if args and len(args) >= 1:
363
- self.builder.set_version(args[0])
364
- elif func_name == "Agent": # CamelCase only
378
+ argument_values = self._extract_arguments(ctx)
379
+
380
+ if function_name == "name":
381
+ if argument_values and len(argument_values) >= 1:
382
+ self.builder.set_name(argument_values[0])
383
+ elif function_name == "version":
384
+ if argument_values and len(argument_values) >= 1:
385
+ self.builder.set_version(argument_values[0])
386
+ elif function_name == "Agent": # CamelCase only
365
387
  # Skip Agent calls inside function bodies - they're runtime lookups, not declarations
366
388
  if self.in_function_body:
367
389
  return self.visitChildren(ctx)
368
390
 
369
- if args and len(args) >= 1: # Support curried syntax with just name
370
- agent_name = args[0]
391
+ if (
392
+ argument_values and len(argument_values) >= 1
393
+ ): # Support curried syntax with just name
394
+ agent_name = argument_values[0]
371
395
  # Check if this is a declaration (has config) or a lookup (just name)
372
- if len(args) >= 2 and isinstance(args[1], dict):
396
+ if len(argument_values) >= 2 and isinstance(argument_values[1], dict):
373
397
  # DEPRECATED: Curried syntax Agent "name" { config }
374
398
  # Raise validation error
375
399
  self.errors.append(
376
400
  ValidationMessage(
377
401
  level="error",
378
- message=f'Curried syntax Agent "{agent_name}" {{...}} is deprecated. Use assignment syntax: {agent_name} = Agent {{...}}',
402
+ message=(
403
+ f'Curried syntax Agent "{agent_name}" {{...}} is deprecated. '
404
+ f"Use assignment syntax: {agent_name} = Agent {{...}}"
405
+ ),
379
406
  location=(self.current_line, self.current_col),
380
407
  declaration="Agent",
381
408
  )
382
409
  )
383
- elif len(args) == 1 and isinstance(agent_name, str):
410
+ elif len(argument_values) == 1 and isinstance(agent_name, str):
384
411
  # DEPRECATED: Agent("name") lookup or curried declaration
385
412
  # This is now invalid - users should use variable references
386
- self.errors.append(
387
- ValidationMessage(
388
- level="error",
389
- message=f'Agent("{agent_name}") lookup syntax is deprecated. Declare the agent with assignment: {agent_name} = Agent {{...}}, then use {agent_name}() to call it.',
390
- location=(self.current_line, self.current_col),
391
- declaration="Agent",
392
- )
413
+ self._record_error(
414
+ message=(
415
+ f'Agent("{agent_name}") lookup syntax is deprecated. '
416
+ f"Declare the agent with assignment: {agent_name} = Agent {{...}}, "
417
+ f"then use {agent_name}() to call it."
418
+ ),
419
+ declaration="Agent",
393
420
  )
394
- elif func_name == "Model": # CamelCase only
395
- if args and len(args) >= 1:
421
+ elif function_name == "Model": # CamelCase only
422
+ if argument_values and len(argument_values) >= 1:
396
423
  # Check if this is assignment syntax (single dict arg) or curried syntax (name + dict)
397
- if len(args) == 1 and isinstance(args[0], dict):
424
+ if len(argument_values) == 1 and isinstance(argument_values[0], dict):
398
425
  # Assignment syntax: my_model = Model {config}
399
426
  # Generate a temp name for validation
400
427
  import uuid
401
428
 
402
429
  temp_name = f"_temp_model_{uuid.uuid4().hex[:8]}"
403
- self.builder.register_model(temp_name, args[0])
404
- elif len(args) >= 2 and isinstance(args[1], dict):
430
+ self.builder.register_model(temp_name, argument_values[0])
431
+ elif len(argument_values) >= 2 and isinstance(argument_values[1], dict):
405
432
  # Curried syntax: Model "name" {config}
406
- config = args[1]
407
- self.builder.register_model(args[0], config)
408
- elif isinstance(args[0], str):
433
+ config = argument_values[1]
434
+ self.builder.register_model(argument_values[0], config)
435
+ elif isinstance(argument_values[0], str):
409
436
  # Just a name, register with empty config
410
- self.builder.register_model(args[0], {})
411
- elif func_name == "Procedure": # CamelCase only
437
+ self.builder.register_model(argument_values[0], {})
438
+ elif function_name == "Procedure": # CamelCase only
412
439
  # Supports multiple syntax variants:
413
440
  # 1. Unnamed (new): Procedure { config with function }
414
441
  # 2. Named (curried): Procedure "name" { config }
415
442
  # 3. Named (old): procedure("name", {config}, function)
416
443
  # Note: args may contain None for unparseable expressions (like functions)
417
- if args and len(args) >= 1:
444
+ if argument_values and len(argument_values) >= 1:
418
445
  # Check if first arg is a table (unnamed procedure syntax)
419
446
  # Tables are parsed as dict if they have named fields, or list if only positional
420
- if isinstance(args[0], dict):
447
+ if isinstance(argument_values[0], dict):
421
448
  # Unnamed syntax: Procedure {...} with named fields
422
449
  # e.g., Procedure { output = {...}, function(input) ... end }
423
450
  proc_name = "main"
424
- config = args[0]
425
- elif isinstance(args[0], list):
451
+ config = argument_values[0]
452
+ elif isinstance(argument_values[0], list):
426
453
  # Unnamed syntax: Procedure {...} with only function (no named fields)
427
454
  # e.g., Procedure { function(input) ... end }
428
455
  # The list contains [None] for the unparseable function
429
456
  proc_name = "main"
430
457
  config = {} # No extractable config from function-only table
431
- elif isinstance(args[0], str):
458
+ elif isinstance(argument_values[0], str):
432
459
  # Named syntax: Procedure "name" {...}
433
- proc_name = args[0]
434
- config = args[1] if len(args) >= 2 and isinstance(args[1], dict) else None
460
+ proc_name = argument_values[0]
461
+ config = (
462
+ argument_values[1]
463
+ if len(argument_values) >= 2 and isinstance(argument_values[1], dict)
464
+ else None
465
+ )
435
466
  else:
436
467
  # Invalid syntax
437
468
  return
@@ -459,109 +490,134 @@ class TactusDSLVisitor(LuaParserVisitor):
459
490
  # Extract inline state schema
460
491
  if "state" in config and isinstance(config["state"], dict):
461
492
  self.builder.register_state_schema(config["state"])
462
- elif func_name == "Prompt": # CamelCase
463
- if args and len(args) >= 2:
464
- self.builder.register_prompt(args[0], args[1])
465
- elif func_name == "Hitl": # CamelCase
466
- if args and len(args) >= 2:
467
- self.builder.register_hitl(args[0], args[1] if isinstance(args[1], dict) else {})
468
- elif func_name == "Specification": # CamelCase
493
+ elif function_name == "Prompt": # CamelCase
494
+ if argument_values and len(argument_values) >= 2:
495
+ self.builder.register_prompt(argument_values[0], argument_values[1])
496
+ elif function_name == "Hitl": # CamelCase
497
+ if argument_values and len(argument_values) >= 2:
498
+ self.builder.register_hitl(
499
+ argument_values[0],
500
+ argument_values[1] if isinstance(argument_values[1], dict) else {},
501
+ )
502
+ elif function_name == "Specification": # CamelCase
469
503
  # Three supported forms:
470
504
  # - Specification([[ Gherkin text ]]) (inline Gherkin)
471
505
  # - Specification("name", { ... }) (structured form; legacy)
472
506
  # - Specification { from = "path" } (external file reference)
473
- if args and len(args) == 1:
474
- arg = args[0]
475
- if isinstance(arg, dict) and "from" in arg:
507
+ if argument_values and len(argument_values) == 1:
508
+ specification_argument = argument_values[0]
509
+ if isinstance(specification_argument, dict) and "from" in specification_argument:
476
510
  # External file reference
477
- self.builder.register_specs_from(arg["from"])
511
+ self.builder.register_specs_from(specification_argument["from"])
478
512
  else:
479
513
  # Inline Gherkin text
480
- self.builder.register_specifications(arg)
481
- elif args and len(args) >= 2:
514
+ self.builder.register_specifications(specification_argument)
515
+ elif argument_values and len(argument_values) >= 2:
482
516
  self.builder.register_specification(
483
- args[0], args[1] if isinstance(args[1], list) else []
517
+ argument_values[0],
518
+ argument_values[1] if isinstance(argument_values[1], list) else [],
484
519
  )
485
- elif func_name == "Specifications": # CamelCase
520
+ elif function_name == "Specifications": # CamelCase
486
521
  # Specifications([[ Gherkin text ]]) (plural form; singular is Specification([[...]]))
487
- if args and len(args) >= 1:
488
- self.builder.register_specifications(args[0])
489
- elif func_name == "Step": # CamelCase
522
+ if argument_values and len(argument_values) >= 1:
523
+ self.builder.register_specifications(argument_values[0])
524
+ elif function_name == "Step": # CamelCase
490
525
  # Step("step text", function() ... end)
491
- if args and len(args) >= 2:
492
- self.builder.register_custom_step(args[0], args[1])
493
- elif func_name == "Evaluation": # CamelCase
526
+ if argument_values and len(argument_values) >= 2:
527
+ self.builder.register_custom_step(argument_values[0], argument_values[1])
528
+ elif function_name == "Evaluation": # CamelCase
494
529
  # Either:
495
530
  # - Evaluation({ runs = 10, parallel = true }) (simple config)
496
531
  # - Evaluation({ dataset = {...}, evaluators = {...}, ... }) (alias for Evaluations)
497
- if args and len(args) >= 1 and isinstance(args[0], dict):
498
- cfg = args[0]
499
- if any(k in cfg for k in ("dataset", "dataset_file", "evaluators", "thresholds")):
500
- self.builder.register_evaluations(cfg)
532
+ if (
533
+ argument_values
534
+ and len(argument_values) >= 1
535
+ and isinstance(argument_values[0], dict)
536
+ ):
537
+ evaluation_config = argument_values[0]
538
+ if any(
539
+ key in evaluation_config
540
+ for key in ("dataset", "dataset_file", "evaluators", "thresholds")
541
+ ):
542
+ self.builder.register_evaluations(evaluation_config)
501
543
  else:
502
- self.builder.set_evaluation_config(cfg)
503
- elif args and len(args) >= 1:
544
+ self.builder.set_evaluation_config(evaluation_config)
545
+ elif argument_values and len(argument_values) >= 1:
504
546
  self.builder.set_evaluation_config({})
505
- elif func_name == "Evaluations": # CamelCase
547
+ elif function_name == "Evaluations": # CamelCase
506
548
  # Evaluation(s)({ dataset = {...}, evaluators = {...} })
507
- if args and len(args) >= 1:
508
- self.builder.register_evaluations(args[0] if isinstance(args[0], dict) else {})
509
- elif func_name == "default_provider":
510
- if args and len(args) >= 1:
511
- self.builder.set_default_provider(args[0])
512
- elif func_name == "default_model":
513
- if args and len(args) >= 1:
514
- self.builder.set_default_model(args[0])
515
- elif func_name == "return_prompt":
516
- if args and len(args) >= 1:
517
- self.builder.set_return_prompt(args[0])
518
- elif func_name == "error_prompt":
519
- if args and len(args) >= 1:
520
- self.builder.set_error_prompt(args[0])
521
- elif func_name == "status_prompt":
522
- if args and len(args) >= 1:
523
- self.builder.set_status_prompt(args[0])
524
- elif func_name == "async":
525
- if args and len(args) >= 1:
526
- self.builder.set_async(args[0])
527
- elif func_name == "max_depth":
528
- if args and len(args) >= 1:
529
- self.builder.set_max_depth(args[0])
530
- elif func_name == "max_turns":
531
- if args and len(args) >= 1:
532
- self.builder.set_max_turns(args[0])
533
- elif func_name == "input":
549
+ if argument_values and len(argument_values) >= 1:
550
+ self.builder.register_evaluations(
551
+ argument_values[0] if isinstance(argument_values[0], dict) else {}
552
+ )
553
+ elif function_name == "default_provider":
554
+ if argument_values and len(argument_values) >= 1:
555
+ self.builder.set_default_provider(argument_values[0])
556
+ elif function_name == "default_model":
557
+ if argument_values and len(argument_values) >= 1:
558
+ self.builder.set_default_model(argument_values[0])
559
+ elif function_name == "return_prompt":
560
+ if argument_values and len(argument_values) >= 1:
561
+ self.builder.set_return_prompt(argument_values[0])
562
+ elif function_name == "error_prompt":
563
+ if argument_values and len(argument_values) >= 1:
564
+ self.builder.set_error_prompt(argument_values[0])
565
+ elif function_name == "status_prompt":
566
+ if argument_values and len(argument_values) >= 1:
567
+ self.builder.set_status_prompt(argument_values[0])
568
+ elif function_name == "async":
569
+ if argument_values and len(argument_values) >= 1:
570
+ self.builder.set_async(argument_values[0])
571
+ elif function_name == "max_depth":
572
+ if argument_values and len(argument_values) >= 1:
573
+ self.builder.set_max_depth(argument_values[0])
574
+ elif function_name == "max_turns":
575
+ if argument_values and len(argument_values) >= 1:
576
+ self.builder.set_max_turns(argument_values[0])
577
+ elif function_name == "input":
534
578
  # Top-level input schema for script mode: input { field1 = ..., field2 = ... }
535
- if args and len(args) >= 1 and isinstance(args[0], dict):
536
- self.builder.register_top_level_input(args[0])
537
- elif func_name == "output":
579
+ if (
580
+ argument_values
581
+ and len(argument_values) >= 1
582
+ and isinstance(argument_values[0], dict)
583
+ ):
584
+ self.builder.register_top_level_input(argument_values[0])
585
+ elif function_name == "output":
538
586
  # Top-level output schema for script mode: output { field1 = ..., field2 = ... }
539
- if args and len(args) >= 1 and isinstance(args[0], dict):
540
- self.builder.register_top_level_output(args[0])
541
- elif func_name == "Tool": # CamelCase only
587
+ if (
588
+ argument_values
589
+ and len(argument_values) >= 1
590
+ and isinstance(argument_values[0], dict)
591
+ ):
592
+ self.builder.register_top_level_output(argument_values[0])
593
+ elif function_name == "Tool": # CamelCase only
542
594
  # Curried syntax (Tool "name" {...} / Tool("name", ...)) is not supported.
543
595
  # Use assignment syntax: my_tool = Tool { ... }.
544
- if args and len(args) >= 1 and isinstance(args[0], str):
545
- tool_name = args[0]
546
- self.errors.append(
547
- ValidationMessage(
548
- level="error",
549
- message=(
550
- f'Curried Tool syntax is not supported: Tool "{tool_name}" {{...}}. '
551
- f"Use assignment syntax: {tool_name} = Tool {{...}}."
552
- ),
553
- location=(self.current_line, self.current_col),
554
- declaration="Tool",
555
- )
596
+ if (
597
+ argument_values
598
+ and len(argument_values) >= 1
599
+ and isinstance(argument_values[0], str)
600
+ ):
601
+ tool_name = argument_values[0]
602
+ self._record_error(
603
+ message=(
604
+ f'Curried Tool syntax is not supported: Tool "{tool_name}" {{...}}. '
605
+ f"Use assignment syntax: {tool_name} = Tool {{...}}."
606
+ ),
607
+ declaration="Tool",
556
608
  )
557
- elif func_name == "Toolset": # CamelCase only
609
+ elif function_name == "Toolset": # CamelCase only
558
610
  # Toolset("name", {config})
559
611
  # or new curried syntax: Toolset "name" { config }
560
- if args and len(args) >= 1: # Support curried syntax
612
+ if argument_values and len(argument_values) >= 1: # Support curried syntax
561
613
  # First arg must be name (string)
562
- if isinstance(args[0], str):
563
- toolset_name = args[0]
564
- config = args[1] if len(args) >= 2 and isinstance(args[1], dict) else {}
614
+ if isinstance(argument_values[0], str):
615
+ toolset_name = argument_values[0]
616
+ config = (
617
+ argument_values[1]
618
+ if len(argument_values) >= 2 and isinstance(argument_values[1], dict)
619
+ else {}
620
+ )
565
621
  # Register the toolset (validation only, no runtime impl yet)
566
622
  self.builder.register_toolset(toolset_name, config)
567
623
 
@@ -573,20 +629,20 @@ class TactusDSLVisitor(LuaParserVisitor):
573
629
  - Unparseable expressions (like functions) are included as None placeholders
574
630
  This allows checking total argument count for validation.
575
631
  """
576
- args = []
632
+ parsed_arguments = []
577
633
 
578
634
  # functioncall has args() children
579
635
  # args: '(' explist? ')' | tableconstructor | LiteralString
580
636
 
581
- args_list = ctx.args()
582
- if not args_list:
583
- return args
637
+ argument_nodes = ctx.args()
638
+ if not argument_nodes:
639
+ return parsed_arguments
584
640
 
585
641
  # Check if this is a method call chain by looking for '.' or ':' between args
586
642
  # For Agent("name").turn({...}), we should only extract "name"
587
643
  # For Procedure "name" {...}, we should extract both "name" and {...}
588
644
  is_method_chain = False
589
- if len(args_list) > 1:
645
+ if len(argument_nodes) > 1:
590
646
  # Check if there's a method access between the first two args
591
647
  # Method chains have pattern: func(arg1).method(arg2)
592
648
  # Shorthand has pattern: func arg1 arg2
@@ -597,9 +653,9 @@ class TactusDSLVisitor(LuaParserVisitor):
597
653
  for i in range(ctx.getChildCount()):
598
654
  child = ctx.getChild(i)
599
655
  # Check if this is the first args
600
- if child == args_list[0]:
656
+ if child == argument_nodes[0]:
601
657
  found_first_args = True
602
- elif found_first_args and child == args_list[1]:
658
+ elif found_first_args and child == argument_nodes[1]:
603
659
  # We've reached the second args without finding . or :
604
660
  # So this is NOT a method chain
605
661
  break
@@ -613,30 +669,30 @@ class TactusDSLVisitor(LuaParserVisitor):
613
669
  # Process arguments
614
670
  if is_method_chain:
615
671
  # Only process first args for method chains like Agent("name").turn(...)
616
- args_to_process = [args_list[0]]
672
+ args_to_process = [argument_nodes[0]]
617
673
  else:
618
674
  # Process all args for shorthand syntax like Procedure "name" {...}
619
- args_to_process = args_list
675
+ args_to_process = argument_nodes
620
676
 
621
677
  for args_ctx in args_to_process:
622
678
  # Check for different argument types
623
679
  if args_ctx.explist():
624
680
  # Regular function call with expression list
625
- explist = args_ctx.explist()
626
- for exp in explist.exp():
627
- value = self._parse_expression(exp)
681
+ expression_list = args_ctx.explist()
682
+ for expression in expression_list.exp():
683
+ value = self._parse_expression(expression)
628
684
  # Include None placeholders to preserve argument count
629
- args.append(value)
685
+ parsed_arguments.append(value)
630
686
  elif args_ctx.tableconstructor():
631
687
  # Table constructor argument
632
688
  table = self._parse_table_constructor(args_ctx.tableconstructor())
633
- args.append(table)
689
+ parsed_arguments.append(table)
634
690
  elif args_ctx.string():
635
691
  # String literal argument
636
692
  string_val = self._parse_string(args_ctx.string())
637
- args.append(string_val)
693
+ parsed_arguments.append(string_val)
638
694
 
639
- return args
695
+ return parsed_arguments
640
696
 
641
697
  def _parse_expression(self, ctx: LuaParser.ExpContext) -> Any:
642
698
  """Parse an expression to a Python value."""
@@ -644,22 +700,22 @@ class TactusDSLVisitor(LuaParserVisitor):
644
700
  return None
645
701
 
646
702
  # Detect field.<type>{...} builder syntax so we can preserve schema info
647
- prefix = ctx.prefixexp()
648
- if prefix and prefix.functioncall():
649
- func_ctx = prefix.functioncall()
650
- name_tokens = [t.getText() for t in func_ctx.NAME()]
703
+ prefix_expression = ctx.prefixexp()
704
+ if prefix_expression and prefix_expression.functioncall():
705
+ function_call_context = prefix_expression.functioncall()
706
+ function_name_tokens = [token.getText() for token in function_call_context.NAME()]
651
707
 
652
708
  # field.string{required = true, ...}
653
- if len(name_tokens) >= 2 and name_tokens[0] == "field":
654
- field_type = name_tokens[-1]
709
+ if len(function_name_tokens) >= 2 and function_name_tokens[0] == "field":
710
+ field_type = function_name_tokens[-1]
655
711
 
656
712
  # Default field definition
657
713
  field_def = {"type": field_type, "required": False}
658
714
 
659
715
  # Parse options table if present
660
- if func_ctx.args():
716
+ if function_call_context.args():
661
717
  # We only expect a single args() entry for the builder
662
- first_arg = func_ctx.args(0)
718
+ first_arg = function_call_context.args(0)
663
719
  if first_arg.tableconstructor():
664
720
  options = self._parse_table_constructor(first_arg.tableconstructor())
665
721
  if isinstance(options, dict):
@@ -707,42 +763,42 @@ class TactusDSLVisitor(LuaParserVisitor):
707
763
 
708
764
  def _parse_string_token(self, token) -> str:
709
765
  """Parse string token to Python string."""
710
- text = token.getText()
766
+ token_text = token.getText()
711
767
 
712
768
  # Handle different Lua string formats
713
- if text.startswith("[[") and text.endswith("]]"):
769
+ if token_text.startswith("[[") and token_text.endswith("]]"):
714
770
  # Long string literal
715
- return text[2:-2]
716
- elif text.startswith('"') and text.endswith('"'):
771
+ return token_text[2:-2]
772
+ elif token_text.startswith('"') and token_text.endswith('"'):
717
773
  # Double-quoted string
718
- content = text[1:-1]
774
+ content = token_text[1:-1]
719
775
  content = content.replace("\\n", "\n")
720
776
  content = content.replace("\\t", "\t")
721
777
  content = content.replace('\\"', '"')
722
778
  content = content.replace("\\\\", "\\")
723
779
  return content
724
- elif text.startswith("'") and text.endswith("'"):
780
+ elif token_text.startswith("'") and token_text.endswith("'"):
725
781
  # Single-quoted string
726
- content = text[1:-1]
782
+ content = token_text[1:-1]
727
783
  content = content.replace("\\n", "\n")
728
784
  content = content.replace("\\t", "\t")
729
785
  content = content.replace("\\'", "'")
730
786
  content = content.replace("\\\\", "\\")
731
787
  return content
732
788
 
733
- return text
789
+ return token_text
734
790
 
735
- def _parse_table_constructor(self, ctx: LuaParser.TableconstructorContext) -> dict:
791
+ def _parse_table_constructor(self, ctx: LuaParser.TableconstructorContext) -> Any:
736
792
  """Parse Lua table constructor to Python dict."""
737
- result = {}
793
+ parsed_table = {}
738
794
  array_items = []
739
795
 
740
796
  if not ctx or not ctx.fieldlist():
741
797
  # Empty table
742
798
  return [] # Return empty list for empty tables (matches runtime behavior)
743
799
 
744
- fieldlist = ctx.fieldlist()
745
- for field in fieldlist.field():
800
+ field_list = ctx.fieldlist()
801
+ for field in field_list.field():
746
802
  # field: '[' exp ']' '=' exp | NAME '=' exp | exp
747
803
  if field.NAME():
748
804
  # Named field: NAME '=' exp
@@ -769,13 +825,16 @@ class TactusDSLVisitor(LuaParserVisitor):
769
825
  self.errors.append(
770
826
  ValidationMessage(
771
827
  level="error",
772
- message=f"Old type syntax detected. Use field.{value}{{}} instead of {{type = '{value}'}}",
828
+ message=(
829
+ f"Old type syntax detected. Use field.{value}{{}} instead of "
830
+ f"{{type = '{value}'}}"
831
+ ),
773
832
  line=field.start.line if field.start else 0,
774
833
  column=field.start.column if field.start else 0,
775
834
  )
776
835
  )
777
836
 
778
- result[key] = value
837
+ parsed_table[key] = value
779
838
  elif len(field.exp()) == 2:
780
839
  # Indexed field: '[' exp ']' '=' exp
781
840
  # Skip for now (complex)
@@ -786,37 +845,37 @@ class TactusDSLVisitor(LuaParserVisitor):
786
845
  array_items.append(value)
787
846
 
788
847
  # If we only have array items, return as list
789
- if array_items and not result:
848
+ if array_items and not parsed_table:
790
849
  return array_items
791
850
 
792
851
  # If we have both, prefer dict (shouldn't happen in DSL)
793
852
  if array_items:
794
853
  # Mixed table - add array items with numeric keys
795
854
  for i, item in enumerate(array_items, 1):
796
- result[i] = item
855
+ parsed_table[i] = item
797
856
 
798
- return result if result else []
857
+ return parsed_table if parsed_table else []
799
858
 
800
- def _parse_number(self, ctx: LuaParser.NumberContext) -> float:
859
+ def _parse_number(self, number_context: LuaParser.NumberContext) -> float:
801
860
  """Parse Lua number to Python number."""
802
- text = ctx.getText()
861
+ number_text = number_context.getText()
803
862
 
804
863
  # Try integer first
805
864
  try:
806
- return int(text)
865
+ return int(number_text)
807
866
  except ValueError:
808
867
  pass
809
868
 
810
869
  # Try float
811
870
  try:
812
- return float(text)
871
+ return float(number_text)
813
872
  except ValueError:
814
873
  pass
815
874
 
816
875
  # Try hex
817
- if text.startswith("0x") or text.startswith("0X"):
876
+ if number_text.startswith("0x") or number_text.startswith("0X"):
818
877
  try:
819
- return int(text, 16)
878
+ return int(number_text, 16)
820
879
  except ValueError:
821
880
  pass
822
881