namel3ss 0.1.0a0__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 (157) hide show
  1. namel3ss/__init__.py +4 -0
  2. namel3ss/ast/__init__.py +5 -0
  3. namel3ss/ast/agents.py +13 -0
  4. namel3ss/ast/ai.py +23 -0
  5. namel3ss/ast/base.py +10 -0
  6. namel3ss/ast/expressions.py +55 -0
  7. namel3ss/ast/nodes.py +86 -0
  8. namel3ss/ast/pages.py +43 -0
  9. namel3ss/ast/program.py +22 -0
  10. namel3ss/ast/records.py +27 -0
  11. namel3ss/ast/statements.py +107 -0
  12. namel3ss/ast/tool.py +11 -0
  13. namel3ss/cli/__init__.py +2 -0
  14. namel3ss/cli/actions_mode.py +39 -0
  15. namel3ss/cli/app_loader.py +22 -0
  16. namel3ss/cli/commands/action.py +27 -0
  17. namel3ss/cli/commands/run.py +43 -0
  18. namel3ss/cli/commands/ui.py +26 -0
  19. namel3ss/cli/commands/validate.py +23 -0
  20. namel3ss/cli/format_mode.py +30 -0
  21. namel3ss/cli/io/json_io.py +19 -0
  22. namel3ss/cli/io/read_source.py +16 -0
  23. namel3ss/cli/json_io.py +21 -0
  24. namel3ss/cli/lint_mode.py +29 -0
  25. namel3ss/cli/main.py +135 -0
  26. namel3ss/cli/new_mode.py +146 -0
  27. namel3ss/cli/runner.py +28 -0
  28. namel3ss/cli/studio_mode.py +22 -0
  29. namel3ss/cli/ui_mode.py +14 -0
  30. namel3ss/config/__init__.py +4 -0
  31. namel3ss/config/dotenv.py +33 -0
  32. namel3ss/config/loader.py +83 -0
  33. namel3ss/config/model.py +49 -0
  34. namel3ss/errors/__init__.py +2 -0
  35. namel3ss/errors/base.py +34 -0
  36. namel3ss/errors/render.py +22 -0
  37. namel3ss/format/__init__.py +3 -0
  38. namel3ss/format/formatter.py +18 -0
  39. namel3ss/format/rules.py +97 -0
  40. namel3ss/ir/__init__.py +3 -0
  41. namel3ss/ir/lowering/__init__.py +4 -0
  42. namel3ss/ir/lowering/agents.py +42 -0
  43. namel3ss/ir/lowering/ai.py +45 -0
  44. namel3ss/ir/lowering/expressions.py +49 -0
  45. namel3ss/ir/lowering/flow.py +21 -0
  46. namel3ss/ir/lowering/pages.py +48 -0
  47. namel3ss/ir/lowering/program.py +34 -0
  48. namel3ss/ir/lowering/records.py +25 -0
  49. namel3ss/ir/lowering/statements.py +122 -0
  50. namel3ss/ir/lowering/tools.py +16 -0
  51. namel3ss/ir/model/__init__.py +50 -0
  52. namel3ss/ir/model/agents.py +33 -0
  53. namel3ss/ir/model/ai.py +31 -0
  54. namel3ss/ir/model/base.py +20 -0
  55. namel3ss/ir/model/expressions.py +50 -0
  56. namel3ss/ir/model/pages.py +43 -0
  57. namel3ss/ir/model/program.py +28 -0
  58. namel3ss/ir/model/statements.py +76 -0
  59. namel3ss/ir/model/tools.py +11 -0
  60. namel3ss/ir/nodes.py +88 -0
  61. namel3ss/lexer/__init__.py +2 -0
  62. namel3ss/lexer/lexer.py +152 -0
  63. namel3ss/lexer/tokens.py +98 -0
  64. namel3ss/lint/__init__.py +4 -0
  65. namel3ss/lint/engine.py +125 -0
  66. namel3ss/lint/semantic.py +45 -0
  67. namel3ss/lint/text_scan.py +70 -0
  68. namel3ss/lint/types.py +22 -0
  69. namel3ss/parser/__init__.py +3 -0
  70. namel3ss/parser/agent.py +78 -0
  71. namel3ss/parser/ai.py +113 -0
  72. namel3ss/parser/constraints.py +37 -0
  73. namel3ss/parser/core.py +166 -0
  74. namel3ss/parser/expressions.py +105 -0
  75. namel3ss/parser/flow.py +37 -0
  76. namel3ss/parser/pages.py +76 -0
  77. namel3ss/parser/program.py +45 -0
  78. namel3ss/parser/records.py +66 -0
  79. namel3ss/parser/statements/__init__.py +27 -0
  80. namel3ss/parser/statements/control_flow.py +116 -0
  81. namel3ss/parser/statements/core.py +66 -0
  82. namel3ss/parser/statements/data.py +17 -0
  83. namel3ss/parser/statements/letset.py +22 -0
  84. namel3ss/parser/statements.py +1 -0
  85. namel3ss/parser/tokens.py +35 -0
  86. namel3ss/parser/tool.py +29 -0
  87. namel3ss/runtime/__init__.py +3 -0
  88. namel3ss/runtime/ai/http/client.py +24 -0
  89. namel3ss/runtime/ai/mock_provider.py +5 -0
  90. namel3ss/runtime/ai/provider.py +29 -0
  91. namel3ss/runtime/ai/providers/__init__.py +18 -0
  92. namel3ss/runtime/ai/providers/_shared/errors.py +20 -0
  93. namel3ss/runtime/ai/providers/_shared/parse.py +18 -0
  94. namel3ss/runtime/ai/providers/anthropic.py +55 -0
  95. namel3ss/runtime/ai/providers/gemini.py +50 -0
  96. namel3ss/runtime/ai/providers/mistral.py +51 -0
  97. namel3ss/runtime/ai/providers/mock.py +23 -0
  98. namel3ss/runtime/ai/providers/ollama.py +39 -0
  99. namel3ss/runtime/ai/providers/openai.py +55 -0
  100. namel3ss/runtime/ai/providers/registry.py +38 -0
  101. namel3ss/runtime/ai/trace.py +18 -0
  102. namel3ss/runtime/executor/__init__.py +3 -0
  103. namel3ss/runtime/executor/agents.py +91 -0
  104. namel3ss/runtime/executor/ai_runner.py +90 -0
  105. namel3ss/runtime/executor/api.py +54 -0
  106. namel3ss/runtime/executor/assign.py +40 -0
  107. namel3ss/runtime/executor/context.py +31 -0
  108. namel3ss/runtime/executor/executor.py +77 -0
  109. namel3ss/runtime/executor/expr_eval.py +110 -0
  110. namel3ss/runtime/executor/records_ops.py +64 -0
  111. namel3ss/runtime/executor/result.py +13 -0
  112. namel3ss/runtime/executor/signals.py +6 -0
  113. namel3ss/runtime/executor/statements.py +99 -0
  114. namel3ss/runtime/memory/manager.py +52 -0
  115. namel3ss/runtime/memory/profile.py +17 -0
  116. namel3ss/runtime/memory/semantic.py +20 -0
  117. namel3ss/runtime/memory/short_term.py +18 -0
  118. namel3ss/runtime/records/service.py +105 -0
  119. namel3ss/runtime/store/__init__.py +2 -0
  120. namel3ss/runtime/store/memory_store.py +62 -0
  121. namel3ss/runtime/tools/registry.py +13 -0
  122. namel3ss/runtime/ui/__init__.py +2 -0
  123. namel3ss/runtime/ui/actions.py +124 -0
  124. namel3ss/runtime/validators/__init__.py +2 -0
  125. namel3ss/runtime/validators/constraints.py +126 -0
  126. namel3ss/schema/__init__.py +2 -0
  127. namel3ss/schema/records.py +52 -0
  128. namel3ss/studio/__init__.py +4 -0
  129. namel3ss/studio/api.py +115 -0
  130. namel3ss/studio/edit/__init__.py +3 -0
  131. namel3ss/studio/edit/ops.py +80 -0
  132. namel3ss/studio/edit/selectors.py +74 -0
  133. namel3ss/studio/edit/transform.py +39 -0
  134. namel3ss/studio/server.py +175 -0
  135. namel3ss/studio/session.py +11 -0
  136. namel3ss/studio/web/app.js +248 -0
  137. namel3ss/studio/web/index.html +44 -0
  138. namel3ss/studio/web/styles.css +42 -0
  139. namel3ss/templates/__init__.py +3 -0
  140. namel3ss/templates/__pycache__/__init__.cpython-312.pyc +0 -0
  141. namel3ss/templates/ai_assistant/.gitignore +1 -0
  142. namel3ss/templates/ai_assistant/README.md +10 -0
  143. namel3ss/templates/ai_assistant/app.ai +30 -0
  144. namel3ss/templates/crud/.gitignore +1 -0
  145. namel3ss/templates/crud/README.md +10 -0
  146. namel3ss/templates/crud/app.ai +26 -0
  147. namel3ss/templates/multi_agent/.gitignore +1 -0
  148. namel3ss/templates/multi_agent/README.md +10 -0
  149. namel3ss/templates/multi_agent/app.ai +43 -0
  150. namel3ss/ui/__init__.py +2 -0
  151. namel3ss/ui/manifest.py +220 -0
  152. namel3ss/utils/__init__.py +2 -0
  153. namel3ss-0.1.0a0.dist-info/METADATA +123 -0
  154. namel3ss-0.1.0a0.dist-info/RECORD +157 -0
  155. namel3ss-0.1.0a0.dist-info/WHEEL +5 -0
  156. namel3ss-0.1.0a0.dist-info/entry_points.txt +2 -0
  157. namel3ss-0.1.0a0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Set
4
+
5
+ from namel3ss.ir import nodes as ir
6
+ from namel3ss.lint.types import Finding
7
+
8
+
9
+ def lint_semantic(program_ir: ir.Program) -> List[Finding]:
10
+ findings: List[Finding] = []
11
+ flow_names: Set[str] = {flow.name for flow in program_ir.flows}
12
+ record_names: Set[str] = {record.name for record in program_ir.records}
13
+ for page in program_ir.pages:
14
+ for item in page.items:
15
+ if isinstance(item, ir.ButtonItem):
16
+ if item.flow_name not in flow_names:
17
+ findings.append(
18
+ Finding(
19
+ code="refs.unknown_flow",
20
+ message=f"Button references unknown flow '{item.flow_name}'",
21
+ line=item.line,
22
+ column=item.column,
23
+ )
24
+ )
25
+ if isinstance(item, ir.FormItem):
26
+ if item.record_name not in record_names:
27
+ findings.append(
28
+ Finding(
29
+ code="refs.unknown_record",
30
+ message=f"Form references unknown record '{item.record_name}'",
31
+ line=item.line,
32
+ column=item.column,
33
+ )
34
+ )
35
+ if isinstance(item, ir.TableItem):
36
+ if item.record_name not in record_names:
37
+ findings.append(
38
+ Finding(
39
+ code="refs.unknown_record",
40
+ message=f"Table references unknown record '{item.record_name}'",
41
+ line=item.line,
42
+ column=item.column,
43
+ )
44
+ )
45
+ return findings
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import List
5
+
6
+ from namel3ss.lint.types import Finding
7
+
8
+
9
+ LEGACY_DECL = re.compile(r'^(flow|page|record|ai|agent|tool)\s+is\s+"')
10
+ ONE_LINE_BUTTON = re.compile(r'^\s*button\s+"[^"]+"\s+calls\s+flow\s+"[^"]+"\s*$')
11
+ PAGE_HEADER = re.compile(r'^\s*page\s+"[^"]+"\s*:\s*$')
12
+ FORBIDDEN_PAGE_TOKENS = {
13
+ "let",
14
+ "set",
15
+ "if",
16
+ "match",
17
+ "repeat",
18
+ "for",
19
+ "try",
20
+ "return",
21
+ "ask",
22
+ "run",
23
+ "save",
24
+ "find",
25
+ }
26
+
27
+
28
+ def scan_text(lines: List[str]) -> List[Finding]:
29
+ findings: List[Finding] = []
30
+ page_indent = None
31
+ for idx, line in enumerate(lines, start=1):
32
+ if LEGACY_DECL.search(line):
33
+ findings.append(
34
+ Finding(
35
+ code="grammar.decl_uses_is",
36
+ message="Declaration uses 'is'; use: <keyword> \"name\":",
37
+ line=idx,
38
+ column=1,
39
+ )
40
+ )
41
+ if ONE_LINE_BUTTON.search(line):
42
+ findings.append(
43
+ Finding(
44
+ code="ui.button_one_line_forbidden",
45
+ message='Buttons must use a block form: button "X": NEWLINE indent calls flow "Y"',
46
+ line=idx,
47
+ column=1,
48
+ )
49
+ )
50
+ if PAGE_HEADER.match(line):
51
+ page_indent = len(line) - len(line.lstrip(" "))
52
+ continue
53
+ if page_indent is not None:
54
+ if line.strip() == "":
55
+ continue
56
+ current_indent = len(line) - len(line.lstrip(" "))
57
+ if current_indent <= page_indent:
58
+ page_indent = None
59
+ continue
60
+ token = line.strip().split()[0].lower()
61
+ if token in FORBIDDEN_PAGE_TOKENS:
62
+ findings.append(
63
+ Finding(
64
+ code="page.imperative_not_allowed",
65
+ message=f"Pages are declarative only; '{token}' is not allowed inside page blocks.",
66
+ line=idx,
67
+ column=current_indent + 1,
68
+ )
69
+ )
70
+ return findings
namel3ss/lint/types.py ADDED
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+
7
+ @dataclass
8
+ class Finding:
9
+ code: str
10
+ message: str
11
+ line: Optional[int]
12
+ column: Optional[int]
13
+ severity: str = "error"
14
+
15
+ def to_dict(self) -> dict:
16
+ return {
17
+ "code": self.code,
18
+ "message": self.message,
19
+ "line": self.line,
20
+ "column": self.column,
21
+ "severity": self.severity,
22
+ }
@@ -0,0 +1,3 @@
1
+ """Parsing pipeline for Namel3ss."""
2
+
3
+ from namel3ss.parser.core import Parser, parse # noqa: F401
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from namel3ss.ast import nodes as ast
4
+ from namel3ss.errors.base import Namel3ssError
5
+
6
+
7
+ def parse_agent_decl(parser) -> ast.AgentDecl:
8
+ agent_tok = parser._advance()
9
+ name_tok = parser._expect("STRING", "Expected agent name string")
10
+ parser._expect("COLON", "Expected ':' after agent name")
11
+ parser._expect("NEWLINE", "Expected newline after agent header")
12
+ parser._expect("INDENT", "Expected indented agent body")
13
+ ai_name = None
14
+ system_prompt = None
15
+ while parser._current().type != "DEDENT":
16
+ if parser._match("NEWLINE"):
17
+ continue
18
+ key_tok = parser._current()
19
+ if key_tok.type == "AI":
20
+ parser._advance()
21
+ parser._expect("IS", "Expected 'is' after ai")
22
+ ai_tok = parser._expect("STRING", "Expected AI profile name")
23
+ ai_name = ai_tok.value
24
+ elif key_tok.type == "SYSTEM_PROMPT":
25
+ parser._advance()
26
+ parser._expect("IS", "Expected 'is' after system_prompt")
27
+ sp_tok = parser._expect("STRING", "Expected system_prompt string")
28
+ system_prompt = sp_tok.value
29
+ else:
30
+ raise Namel3ssError("Unknown field in agent declaration", line=key_tok.line, column=key_tok.column)
31
+ parser._match("NEWLINE")
32
+ parser._expect("DEDENT", "Expected end of agent body")
33
+ if ai_name is None:
34
+ raise Namel3ssError("Agent requires an AI profile", line=agent_tok.line, column=agent_tok.column)
35
+ return ast.AgentDecl(name=name_tok.value, ai_name=ai_name, system_prompt=system_prompt, line=agent_tok.line, column=agent_tok.column)
36
+
37
+
38
+ def parse_run_agent_stmt(parser) -> ast.RunAgentStmt:
39
+ run_tok = parser._advance()
40
+ if not parser._match("AGENT"):
41
+ raise Namel3ssError("Expected 'agent' after run", line=run_tok.line, column=run_tok.column)
42
+ name_tok = parser._expect("STRING", "Expected agent name string")
43
+ parser._expect("WITH", "Expected 'with' in run agent")
44
+ parser._expect("INPUT", "Expected 'input' in run agent")
45
+ parser._expect("COLON", "Expected ':' after input")
46
+ input_expr = parser._parse_expression()
47
+ parser._expect("AS", "Expected 'as' to bind agent result")
48
+ target_tok = parser._expect("IDENT", "Expected target identifier after 'as'")
49
+ return ast.RunAgentStmt(agent_name=name_tok.value, input_expr=input_expr, target=target_tok.value, line=run_tok.line, column=run_tok.column)
50
+
51
+
52
+ def parse_run_agents_parallel(parser) -> ast.RunAgentsParallelStmt:
53
+ run_tok = parser._advance()
54
+ if not parser._match("AGENTS"):
55
+ raise Namel3ssError("Expected 'agents' after run", line=run_tok.line, column=run_tok.column)
56
+ parser._expect("IN", "Expected 'in'")
57
+ parser._expect("PARALLEL", "Expected 'parallel'")
58
+ parser._expect("COLON", "Expected ':' after parallel header")
59
+ parser._expect("NEWLINE", "Expected newline after parallel header")
60
+ parser._expect("INDENT", "Expected indented parallel block")
61
+ entries: list[ast.ParallelAgentEntry] = []
62
+ while parser._current().type != "DEDENT":
63
+ if parser._match("NEWLINE"):
64
+ continue
65
+ parser._expect("AGENT", "Expected 'agent' in parallel block")
66
+ name_tok = parser._expect("STRING", "Expected agent name string")
67
+ parser._expect("WITH", "Expected 'with' in agent entry")
68
+ parser._expect("INPUT", "Expected 'input' in agent entry")
69
+ parser._expect("COLON", "Expected ':' after input")
70
+ input_expr = parser._parse_expression()
71
+ entries.append(ast.ParallelAgentEntry(agent_name=name_tok.value, input_expr=input_expr, line=name_tok.line, column=name_tok.column))
72
+ parser._match("NEWLINE")
73
+ parser._expect("DEDENT", "Expected end of parallel agents block")
74
+ if not entries:
75
+ raise Namel3ssError("Parallel agent block requires at least one entry", line=run_tok.line, column=run_tok.column)
76
+ parser._expect("AS", "Expected 'as' after parallel block")
77
+ target_tok = parser._expect("IDENT", "Expected target identifier after 'as'")
78
+ return ast.RunAgentsParallelStmt(entries=entries, target=target_tok.value, line=run_tok.line, column=run_tok.column)
namel3ss/parser/ai.py ADDED
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from namel3ss.ast import nodes as ast_nodes
4
+ from namel3ss.errors.base import Namel3ssError
5
+
6
+
7
+ def parse_ai_decl(parser) -> ast_nodes.AIDecl:
8
+ ai_tok = parser._advance()
9
+ name_tok = parser._expect("STRING", "Expected AI name string")
10
+ parser._expect("COLON", "Expected ':' after AI name")
11
+ parser._expect("NEWLINE", "Expected newline after AI header")
12
+ parser._expect("INDENT", "Expected indented AI body")
13
+ model = None
14
+ provider = None
15
+ system_prompt = None
16
+ exposed_tools: list[str] = []
17
+ memory = ast_nodes.AIMemory(line=ai_tok.line, column=ai_tok.column)
18
+ while parser._current().type != "DEDENT":
19
+ if parser._match("NEWLINE"):
20
+ continue
21
+ key_tok = parser._current()
22
+ if key_tok.type == "MODEL":
23
+ parser._advance()
24
+ parser._expect("IS", "Expected 'is' after model")
25
+ value_tok = parser._expect("STRING", "Expected model string")
26
+ model = value_tok.value
27
+ elif key_tok.type == "PROVIDER":
28
+ parser._advance()
29
+ parser._expect("IS", "Expected 'is' after provider")
30
+ value_tok = parser._expect("STRING", "Expected provider string")
31
+ provider = value_tok.value
32
+ elif key_tok.type == "SYSTEM_PROMPT":
33
+ parser._advance()
34
+ parser._expect("IS", "Expected 'is' after system_prompt")
35
+ value_tok = parser._expect("STRING", "Expected system_prompt string")
36
+ system_prompt = value_tok.value
37
+ elif key_tok.type == "TOOLS":
38
+ parser._advance()
39
+ parser._expect("COLON", "Expected ':' after tools")
40
+ parser._expect("NEWLINE", "Expected newline after tools:")
41
+ parser._expect("INDENT", "Expected indented tools block")
42
+ while parser._current().type != "DEDENT":
43
+ if parser._match("NEWLINE"):
44
+ continue
45
+ if parser._match("EXPOSE"):
46
+ tool_tok = parser._expect("STRING", "Expected tool name string")
47
+ tool_name = tool_tok.value
48
+ if tool_name in exposed_tools:
49
+ raise Namel3ssError(f"Duplicate tool exposure '{tool_name}'", line=tool_tok.line, column=tool_tok.column)
50
+ exposed_tools.append(tool_name)
51
+ else:
52
+ raise Namel3ssError("Unknown entry in tools block", line=parser._current().line, column=parser._current().column)
53
+ parser._match("NEWLINE")
54
+ parser._expect("DEDENT", "Expected end of tools block")
55
+ elif key_tok.type == "MEMORY":
56
+ parser._advance()
57
+ parser._expect("COLON", "Expected ':' after memory")
58
+ parser._expect("NEWLINE", "Expected newline after memory:")
59
+ parser._expect("INDENT", "Expected indented memory block")
60
+ while parser._current().type != "DEDENT":
61
+ if parser._match("NEWLINE"):
62
+ continue
63
+ mem_key = parser._current()
64
+ if mem_key.type == "SHORT_TERM":
65
+ parser._advance()
66
+ parser._expect("IS", "Expected 'is' after short_term")
67
+ value_tok = parser._expect("NUMBER", "short_term must be a number literal")
68
+ if not isinstance(value_tok.value, int) or value_tok.value < 0:
69
+ raise Namel3ssError("short_term must be a non-negative integer", line=value_tok.line, column=value_tok.column)
70
+ memory.short_term = value_tok.value
71
+ elif mem_key.type == "SEMANTIC":
72
+ parser._advance()
73
+ parser._expect("IS", "Expected 'is' after semantic")
74
+ bool_tok = parser._expect("BOOLEAN", "semantic must be true/false literal")
75
+ memory.semantic = bool_tok.value
76
+ elif mem_key.type == "PROFILE":
77
+ parser._advance()
78
+ parser._expect("IS", "Expected 'is' after profile")
79
+ bool_tok = parser._expect("BOOLEAN", "profile must be true/false literal")
80
+ memory.profile = bool_tok.value
81
+ else:
82
+ raise Namel3ssError("Unknown memory field", line=mem_key.line, column=mem_key.column)
83
+ parser._match("NEWLINE")
84
+ parser._expect("DEDENT", "Expected end of memory block")
85
+ else:
86
+ raise Namel3ssError("Unknown field in AI declaration", line=key_tok.line, column=key_tok.column)
87
+ parser._match("NEWLINE")
88
+ parser._expect("DEDENT", "Expected end of AI body")
89
+ if model is None:
90
+ raise Namel3ssError("AI declaration requires a model", line=ai_tok.line, column=ai_tok.column)
91
+ return ast_nodes.AIDecl(
92
+ name=name_tok.value,
93
+ model=model,
94
+ provider=provider,
95
+ system_prompt=system_prompt,
96
+ exposed_tools=exposed_tools,
97
+ memory=memory,
98
+ line=ai_tok.line,
99
+ column=ai_tok.column,
100
+ )
101
+
102
+
103
+ def parse_ask_stmt(parser) -> ast_nodes.AskAIStmt:
104
+ ask_tok = parser._advance()
105
+ parser._expect("AI", "Expected 'ai' after 'ask'")
106
+ name_tok = parser._expect("STRING", "Expected AI name string")
107
+ parser._expect("WITH", "Expected 'with' in ask ai statement")
108
+ parser._expect("INPUT", "Expected 'input' in ask ai statement")
109
+ parser._expect("COLON", "Expected ':' after input")
110
+ input_expr = parser._parse_expression()
111
+ parser._expect("AS", "Expected 'as' to bind AI result")
112
+ target_tok = parser._expect("IDENT", "Expected target identifier after 'as'")
113
+ return ast_nodes.AskAIStmt(ai_name=name_tok.value, input_expr=input_expr, target=target_tok.value, line=ask_tok.line, column=ask_tok.column)
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from namel3ss.ast import nodes as ast
4
+ from namel3ss.errors.base import Namel3ssError
5
+
6
+
7
+ def parse_field_constraint(parser) -> ast.FieldConstraint:
8
+ tok = parser._current()
9
+ if parser._match("BE"):
10
+ if parser._match("PRESENT"):
11
+ return ast.FieldConstraint(kind="present", line=tok.line, column=tok.column)
12
+ if parser._match("UNIQUE"):
13
+ return ast.FieldConstraint(kind="unique", line=tok.line, column=tok.column)
14
+ if parser._match("GREATER"):
15
+ parser._expect("THAN", "Expected 'than' after 'greater'")
16
+ expr = parser._parse_expression()
17
+ return ast.FieldConstraint(kind="gt", expression=expr, line=tok.line, column=tok.column)
18
+ if parser._match("LESS"):
19
+ parser._expect("THAN", "Expected 'than' after 'less'")
20
+ expr = parser._parse_expression()
21
+ return ast.FieldConstraint(kind="lt", expression=expr, line=tok.line, column=tok.column)
22
+ if parser._match("MATCH"):
23
+ parser._expect("PATTERN", "Expected 'pattern'")
24
+ pattern_tok = parser._expect("STRING", "Expected pattern string")
25
+ return ast.FieldConstraint(kind="pattern", pattern=pattern_tok.value, line=tok.line, column=tok.column)
26
+ if parser._match("HAVE"):
27
+ parser._expect("LENGTH", "Expected 'length'")
28
+ parser._expect("AT", "Expected 'at'")
29
+ if parser._match("LEAST"):
30
+ expr = parser._parse_expression()
31
+ return ast.FieldConstraint(kind="len_min", expression=expr, line=tok.line, column=tok.column)
32
+ if parser._match("MOST"):
33
+ expr = parser._parse_expression()
34
+ return ast.FieldConstraint(kind="len_max", expression=expr, line=tok.line, column=tok.column)
35
+ tok = parser._current()
36
+ raise Namel3ssError("Expected 'least' or 'most' after length", line=tok.line, column=tok.column)
37
+ raise Namel3ssError("Unknown constraint", line=tok.line, column=tok.column)
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Set
4
+
5
+ from namel3ss.ast import nodes as ast
6
+ from namel3ss.lexer.lexer import Lexer
7
+ from namel3ss.lexer.tokens import Token
8
+ from namel3ss.parser import tokens as token_ops
9
+ from namel3ss.parser.constraints import parse_field_constraint
10
+ from namel3ss.parser.expressions import (
11
+ parse_comparison,
12
+ parse_expression,
13
+ parse_not,
14
+ parse_or,
15
+ parse_primary,
16
+ parse_state_path,
17
+ )
18
+ from namel3ss.parser.expressions import parse_and as parse_and_expr
19
+ from namel3ss.parser.flow import parse_block, parse_flow, parse_statements
20
+ from namel3ss.parser.pages import parse_page, parse_page_item
21
+ from namel3ss.parser.program import parse_program
22
+ from namel3ss.parser.records import parse_record, parse_record_fields, type_from_token
23
+ from namel3ss.parser.statements import (
24
+ parse_find,
25
+ parse_for_each,
26
+ parse_if,
27
+ parse_let,
28
+ parse_match,
29
+ parse_repeat,
30
+ parse_return,
31
+ parse_save,
32
+ parse_set,
33
+ parse_statement,
34
+ parse_target,
35
+ parse_try,
36
+ validate_match_pattern,
37
+ )
38
+
39
+
40
+ class Parser:
41
+ def __init__(self, tokens: List[Token]) -> None:
42
+ self.tokens = tokens
43
+ self.position = 0
44
+
45
+ @classmethod
46
+ def parse(cls, source: str) -> ast.Program:
47
+ lexer = Lexer(source)
48
+ tokens = lexer.tokenize()
49
+ parser = cls(tokens)
50
+ program = parser._parse_program()
51
+ parser._expect("EOF")
52
+ return program
53
+
54
+ # Token helpers
55
+ def _current(self) -> Token:
56
+ return token_ops.current(self)
57
+
58
+ def _advance(self) -> Token:
59
+ return token_ops.advance(self)
60
+
61
+ def _match(self, *types: str) -> bool:
62
+ return token_ops.match(self, *types)
63
+
64
+ def _expect(self, token_type: str, message=None) -> Token:
65
+ return token_ops.expect(self, token_type, message)
66
+
67
+ # Program level
68
+ def _parse_program(self) -> ast.Program:
69
+ return parse_program(self)
70
+
71
+ # Flow and blocks
72
+ def _parse_flow(self) -> ast.Flow:
73
+ return parse_flow(self)
74
+
75
+ def _parse_statements(self, until: Set[str]) -> List[ast.Statement]:
76
+ return parse_statements(self, until)
77
+
78
+ def _parse_block(self) -> List[ast.Statement]:
79
+ return parse_block(self)
80
+
81
+ # Statements
82
+ def _parse_statement(self) -> ast.Statement:
83
+ return parse_statement(self)
84
+
85
+ def _parse_let(self) -> ast.Let:
86
+ return parse_let(self)
87
+
88
+ def _parse_set(self) -> ast.Set:
89
+ return parse_set(self)
90
+
91
+ def _parse_if(self) -> ast.If:
92
+ return parse_if(self)
93
+
94
+ def _parse_return(self) -> ast.Return:
95
+ return parse_return(self)
96
+
97
+ def _parse_repeat(self) -> ast.Repeat:
98
+ return parse_repeat(self)
99
+
100
+ def _parse_for_each(self) -> ast.ForEach:
101
+ return parse_for_each(self)
102
+
103
+ def _parse_match(self) -> ast.Match:
104
+ return parse_match(self)
105
+
106
+ def _parse_try(self) -> ast.TryCatch:
107
+ return parse_try(self)
108
+
109
+ def _parse_save(self) -> ast.Save:
110
+ return parse_save(self)
111
+
112
+ def _parse_find(self) -> ast.Find:
113
+ return parse_find(self)
114
+
115
+ def _parse_target(self) -> ast.Assignable:
116
+ return parse_target(self)
117
+
118
+ def _validate_match_pattern(self, pattern: ast.Expression) -> None:
119
+ return validate_match_pattern(self, pattern)
120
+
121
+ # Expressions
122
+ def _parse_expression(self) -> ast.Expression:
123
+ return parse_expression(self)
124
+
125
+ def _parse_or(self) -> ast.Expression:
126
+ return parse_or(self)
127
+
128
+ def _parse_and(self) -> ast.Expression:
129
+ return parse_and_expr(self)
130
+
131
+ def _parse_not(self) -> ast.Expression:
132
+ return parse_not(self)
133
+
134
+ def _parse_comparison(self) -> ast.Expression:
135
+ return parse_comparison(self)
136
+
137
+ def _parse_primary(self) -> ast.Expression:
138
+ return parse_primary(self)
139
+
140
+ def _parse_state_path(self) -> ast.StatePath:
141
+ return parse_state_path(self)
142
+
143
+ # Records and constraints
144
+ def _parse_record(self) -> ast.RecordDecl:
145
+ return parse_record(self)
146
+
147
+ def _parse_record_fields(self) -> List[ast.FieldDecl]:
148
+ return parse_record_fields(self)
149
+
150
+ def _parse_field_constraint(self) -> ast.FieldConstraint:
151
+ return parse_field_constraint(self)
152
+
153
+ @staticmethod
154
+ def _type_from_token(tok: Token) -> str:
155
+ return type_from_token(tok)
156
+
157
+ # Pages
158
+ def _parse_page(self) -> ast.PageDecl:
159
+ return parse_page(self)
160
+
161
+ def _parse_page_item(self) -> ast.PageItem:
162
+ return parse_page_item(self)
163
+
164
+
165
+ def parse(source: str) -> ast.Program:
166
+ return Parser.parse(source)
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+
5
+ from namel3ss.ast import nodes as ast
6
+ from namel3ss.errors.base import Namel3ssError
7
+
8
+
9
+ def parse_expression(parser) -> ast.Expression:
10
+ return parse_or(parser)
11
+
12
+
13
+ def parse_or(parser) -> ast.Expression:
14
+ expr = parse_and(parser)
15
+ while parser._match("OR"):
16
+ op_tok = parser.tokens[parser.position - 1]
17
+ right = parse_and(parser)
18
+ expr = ast.BinaryOp(op="or", left=expr, right=right, line=op_tok.line, column=op_tok.column)
19
+ return expr
20
+
21
+
22
+ def parse_and(parser) -> ast.Expression:
23
+ expr = parse_not(parser)
24
+ while parser._match("AND"):
25
+ op_tok = parser.tokens[parser.position - 1]
26
+ right = parse_not(parser)
27
+ expr = ast.BinaryOp(op="and", left=expr, right=right, line=op_tok.line, column=op_tok.column)
28
+ return expr
29
+
30
+
31
+ def parse_not(parser) -> ast.Expression:
32
+ if parser._match("NOT"):
33
+ tok = parser.tokens[parser.position - 1]
34
+ operand = parse_not(parser)
35
+ return ast.UnaryOp(op="not", operand=operand, line=tok.line, column=tok.column)
36
+ return parse_comparison(parser)
37
+
38
+
39
+ def parse_comparison(parser) -> ast.Expression:
40
+ left = parse_primary(parser)
41
+ if not parser._match("IS"):
42
+ return left
43
+ is_tok = parser.tokens[parser.position - 1]
44
+ if parser._match("GREATER"):
45
+ parser._expect("THAN", "Expected 'than' after 'is greater'")
46
+ right = parse_primary(parser)
47
+ return ast.Comparison(kind="gt", left=left, right=right, line=is_tok.line, column=is_tok.column)
48
+ if parser._match("LESS"):
49
+ parser._expect("THAN", "Expected 'than' after 'is less'")
50
+ right = parse_primary(parser)
51
+ return ast.Comparison(kind="lt", left=left, right=right, line=is_tok.line, column=is_tok.column)
52
+ if parser._match("EQUAL"):
53
+ if parser._match("TO"):
54
+ pass
55
+ right = parse_primary(parser)
56
+ return ast.Comparison(kind="eq", left=left, right=right, line=is_tok.line, column=is_tok.column)
57
+ right = parse_primary(parser)
58
+ return ast.Comparison(kind="eq", left=left, right=right, line=is_tok.line, column=is_tok.column)
59
+
60
+
61
+ def parse_primary(parser) -> ast.Expression:
62
+ tok = parser._current()
63
+ if tok.type == "NUMBER":
64
+ parser._advance()
65
+ return ast.Literal(value=tok.value, line=tok.line, column=tok.column)
66
+ if tok.type == "STRING":
67
+ parser._advance()
68
+ return ast.Literal(value=tok.value, line=tok.line, column=tok.column)
69
+ if tok.type == "BOOLEAN":
70
+ parser._advance()
71
+ return ast.Literal(value=tok.value, line=tok.line, column=tok.column)
72
+ if tok.type in {"IDENT", "INPUT"}:
73
+ parser._advance()
74
+ attrs: List[str] = []
75
+ while parser._match("DOT"):
76
+ attr_tok = parser._expect("IDENT", "Expected identifier after '.'")
77
+ attrs.append(attr_tok.value)
78
+ if attrs:
79
+ return ast.AttrAccess(base=tok.value, attrs=attrs, line=tok.line, column=tok.column)
80
+ return ast.VarReference(name=tok.value, line=tok.line, column=tok.column)
81
+ if tok.type == "STATE":
82
+ return parse_state_path(parser)
83
+ if tok.type == "LPAREN":
84
+ parser._advance()
85
+ expr = parse_expression(parser)
86
+ parser._expect("RPAREN", "Expected ')'")
87
+ return expr
88
+ if tok.type == "ASK":
89
+ raise Namel3ssError(
90
+ 'AI calls are statements. Use: ask ai "name" with input: <expr> as <target>.',
91
+ line=tok.line,
92
+ column=tok.column,
93
+ )
94
+ raise Namel3ssError("Unexpected expression", line=tok.line, column=tok.column)
95
+
96
+
97
+ def parse_state_path(parser) -> ast.StatePath:
98
+ state_tok = parser._expect("STATE", "Expected 'state'")
99
+ path: List[str] = []
100
+ while parser._match("DOT"):
101
+ ident_tok = parser._expect("IDENT", "Expected identifier after '.'")
102
+ path.append(ident_tok.value)
103
+ if not path:
104
+ raise Namel3ssError("Expected state path after 'state'", line=state_tok.line, column=state_tok.column)
105
+ return ast.StatePath(path=path, line=state_tok.line, column=state_tok.column)