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,50 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List, Union
5
+
6
+ from namel3ss.ir.model.base import Expression
7
+
8
+
9
+ @dataclass
10
+ class Literal(Expression):
11
+ value: Union[str, int, bool]
12
+
13
+
14
+ @dataclass
15
+ class VarReference(Expression):
16
+ name: str
17
+
18
+
19
+ @dataclass
20
+ class AttrAccess(Expression):
21
+ base: str
22
+ attrs: List[str]
23
+
24
+
25
+ @dataclass
26
+ class StatePath(Expression):
27
+ path: List[str]
28
+
29
+
30
+ @dataclass
31
+ class UnaryOp(Expression):
32
+ op: str
33
+ operand: Expression
34
+
35
+
36
+ @dataclass
37
+ class BinaryOp(Expression):
38
+ op: str
39
+ left: Expression
40
+ right: Expression
41
+
42
+
43
+ @dataclass
44
+ class Comparison(Expression):
45
+ kind: str
46
+ left: Expression
47
+ right: Expression
48
+
49
+
50
+ Assignable = Union[VarReference, StatePath]
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List
5
+
6
+ from namel3ss.ir.model.base import Node
7
+
8
+
9
+ @dataclass
10
+ class Page(Node):
11
+ name: str
12
+ items: List["PageItem"]
13
+
14
+
15
+ @dataclass
16
+ class PageItem(Node):
17
+ pass
18
+
19
+
20
+ @dataclass
21
+ class TitleItem(PageItem):
22
+ value: str
23
+
24
+
25
+ @dataclass
26
+ class TextItem(PageItem):
27
+ value: str
28
+
29
+
30
+ @dataclass
31
+ class FormItem(PageItem):
32
+ record_name: str
33
+
34
+
35
+ @dataclass
36
+ class TableItem(PageItem):
37
+ record_name: str
38
+
39
+
40
+ @dataclass
41
+ class ButtonItem(PageItem):
42
+ label: str
43
+ flow_name: str
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Dict, List
5
+
6
+ from namel3ss.ir.model.base import Node
7
+ from namel3ss.ir.model.statements import Statement
8
+ from namel3ss.ir.model.pages import Page
9
+ from namel3ss.ir.model.ai import AIDecl
10
+ from namel3ss.ir.model.agents import AgentDecl
11
+ from namel3ss.ir.model.tools import ToolDecl
12
+ from namel3ss.schema import records as schema
13
+
14
+
15
+ @dataclass
16
+ class Flow(Node):
17
+ name: str
18
+ body: List[Statement]
19
+
20
+
21
+ @dataclass
22
+ class Program(Node):
23
+ records: List[schema.RecordSchema]
24
+ flows: List[Flow]
25
+ pages: List[Page]
26
+ ais: Dict[str, AIDecl]
27
+ tools: Dict[str, ToolDecl]
28
+ agents: Dict[str, AgentDecl]
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List
5
+
6
+ from namel3ss.ir.model.base import Node, Statement
7
+ from namel3ss.ir.model.expressions import Assignable, Expression
8
+
9
+
10
+ @dataclass
11
+ class Let(Statement):
12
+ name: str
13
+ expression: Expression
14
+ constant: bool
15
+
16
+
17
+ @dataclass
18
+ class Set(Statement):
19
+ target: Assignable
20
+ expression: Expression
21
+
22
+
23
+ @dataclass
24
+ class If(Statement):
25
+ condition: Expression
26
+ then_body: List[Statement]
27
+ else_body: List[Statement]
28
+
29
+
30
+ @dataclass
31
+ class Return(Statement):
32
+ expression: Expression
33
+
34
+
35
+ @dataclass
36
+ class Repeat(Statement):
37
+ count: Expression
38
+ body: List[Statement]
39
+
40
+
41
+ @dataclass
42
+ class ForEach(Statement):
43
+ name: str
44
+ iterable: Expression
45
+ body: List[Statement]
46
+
47
+
48
+ @dataclass
49
+ class MatchCase(Node):
50
+ pattern: Expression
51
+ body: List[Statement]
52
+
53
+
54
+ @dataclass
55
+ class Match(Statement):
56
+ expression: Expression
57
+ cases: List[MatchCase]
58
+ otherwise: List[Statement] | None
59
+
60
+
61
+ @dataclass
62
+ class TryCatch(Statement):
63
+ try_body: List[Statement]
64
+ catch_var: str
65
+ catch_body: List[Statement]
66
+
67
+
68
+ @dataclass
69
+ class Save(Statement):
70
+ record_name: str
71
+
72
+
73
+ @dataclass
74
+ class Find(Statement):
75
+ record_name: str
76
+ predicate: Expression
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from namel3ss.ir.model.base import Node
6
+
7
+
8
+ @dataclass
9
+ class ToolDecl(Node):
10
+ name: str
11
+ kind: str
namel3ss/ir/nodes.py ADDED
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from namel3ss.ir.lowering import lower_program, lower_flow
4
+ from namel3ss.ir.model import ( # noqa: F401
5
+ AIMemory,
6
+ AIDecl,
7
+ AgentDecl,
8
+ AskAIStmt,
9
+ Assignable,
10
+ AttrAccess,
11
+ BinaryOp,
12
+ ButtonItem,
13
+ Comparison,
14
+ Expression,
15
+ Find,
16
+ Flow,
17
+ ForEach,
18
+ FormItem,
19
+ If,
20
+ Let,
21
+ Literal,
22
+ Match,
23
+ MatchCase,
24
+ Node,
25
+ Page,
26
+ PageItem,
27
+ ParallelAgentEntry,
28
+ Program,
29
+ Repeat,
30
+ Return,
31
+ RunAgentStmt,
32
+ RunAgentsParallelStmt,
33
+ Save,
34
+ Set,
35
+ StatePath,
36
+ Statement,
37
+ TableItem,
38
+ TextItem,
39
+ TitleItem,
40
+ ToolDecl,
41
+ TryCatch,
42
+ UnaryOp,
43
+ VarReference,
44
+ )
45
+
46
+ __all__ = [
47
+ "AIMemory",
48
+ "AIDecl",
49
+ "AgentDecl",
50
+ "AskAIStmt",
51
+ "Assignable",
52
+ "AttrAccess",
53
+ "BinaryOp",
54
+ "ButtonItem",
55
+ "Comparison",
56
+ "Expression",
57
+ "Find",
58
+ "Flow",
59
+ "ForEach",
60
+ "FormItem",
61
+ "If",
62
+ "Let",
63
+ "Literal",
64
+ "Match",
65
+ "MatchCase",
66
+ "Node",
67
+ "Page",
68
+ "PageItem",
69
+ "ParallelAgentEntry",
70
+ "Program",
71
+ "Repeat",
72
+ "Return",
73
+ "RunAgentStmt",
74
+ "RunAgentsParallelStmt",
75
+ "Save",
76
+ "Set",
77
+ "StatePath",
78
+ "Statement",
79
+ "TableItem",
80
+ "TextItem",
81
+ "TitleItem",
82
+ "ToolDecl",
83
+ "TryCatch",
84
+ "UnaryOp",
85
+ "VarReference",
86
+ "lower_program",
87
+ "lower_flow",
88
+ ]
@@ -0,0 +1,2 @@
1
+ """Lexical analysis utilities for Namel3ss."""
2
+
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List
4
+
5
+ from namel3ss.errors.base import Namel3ssError
6
+ from namel3ss.lexer.tokens import KEYWORDS, Token
7
+
8
+
9
+ class Lexer:
10
+ """Line-based lexer with indentation awareness."""
11
+
12
+ def __init__(self, source: str) -> None:
13
+ self.source = source
14
+
15
+ def tokenize(self) -> List[Token]:
16
+ tokens: List[Token] = []
17
+ indent_stack = [0]
18
+ lines = self.source.splitlines()
19
+
20
+ for line_no, raw_line in enumerate(lines, start=1):
21
+ stripped = raw_line.rstrip("\n")
22
+ if stripped.strip() == "":
23
+ continue
24
+
25
+ indent = self._leading_spaces(stripped)
26
+ if indent > indent_stack[-1]:
27
+ tokens.append(Token("INDENT", None, line_no, 1))
28
+ indent_stack.append(indent)
29
+ else:
30
+ while indent < indent_stack[-1]:
31
+ indent_stack.pop()
32
+ tokens.append(Token("DEDENT", None, line_no, 1))
33
+ if indent != indent_stack[-1]:
34
+ raise Namel3ssError(
35
+ f"Inconsistent indentation (got {indent} spaces, expected {indent_stack[-1]})",
36
+ line=line_no,
37
+ column=1,
38
+ )
39
+
40
+ line_tokens = self._scan_line(stripped.lstrip(" "), line_no, indent + 1)
41
+ tokens.extend(line_tokens)
42
+ tokens.append(Token("NEWLINE", None, line_no, len(stripped) + 1))
43
+
44
+ while len(indent_stack) > 1:
45
+ indent_stack.pop()
46
+ tokens.append(Token("DEDENT", None, len(lines), 1))
47
+
48
+ tokens.append(Token("EOF", None, len(lines) + 1, 1))
49
+ return tokens
50
+
51
+ @staticmethod
52
+ def _leading_spaces(text: str) -> int:
53
+ count = 0
54
+ for ch in text:
55
+ if ch == " ":
56
+ count += 1
57
+ else:
58
+ break
59
+ return count
60
+
61
+ def _scan_line(self, text: str, line_no: int, start_col: int) -> List[Token]:
62
+ tokens: List[Token] = []
63
+ i = 0
64
+ column = start_col
65
+ while i < len(text):
66
+ ch = text[i]
67
+ if ch == " ":
68
+ i += 1
69
+ column += 1
70
+ continue
71
+ if ch == ":":
72
+ tokens.append(Token("COLON", ":", line_no, column))
73
+ i += 1
74
+ column += 1
75
+ continue
76
+ if ch == ".":
77
+ tokens.append(Token("DOT", ".", line_no, column))
78
+ i += 1
79
+ column += 1
80
+ continue
81
+ if ch == "(":
82
+ tokens.append(Token("LPAREN", "(", line_no, column))
83
+ i += 1
84
+ column += 1
85
+ continue
86
+ if ch == ")":
87
+ tokens.append(Token("RPAREN", ")", line_no, column))
88
+ i += 1
89
+ column += 1
90
+ continue
91
+ if ch == '"':
92
+ value, consumed = self._read_string(text[i:], line_no, column)
93
+ tokens.append(Token("STRING", value, line_no, column))
94
+ i += consumed
95
+ column += consumed
96
+ continue
97
+ if ch.isdigit():
98
+ value, consumed = self._read_number(text[i:])
99
+ tokens.append(Token("NUMBER", value, line_no, column))
100
+ i += consumed
101
+ column += consumed
102
+ continue
103
+ if ch.isalpha() or ch == "_":
104
+ value, consumed = self._read_identifier(text[i:])
105
+ token_type = KEYWORDS.get(value, "IDENT")
106
+ token_value = self._keyword_value(token_type, value)
107
+ tokens.append(Token(token_type, token_value, line_no, column))
108
+ i += consumed
109
+ column += consumed
110
+ continue
111
+
112
+ raise Namel3ssError(f"Unexpected character '{ch}'", line=line_no, column=column)
113
+
114
+ return tokens
115
+
116
+ @staticmethod
117
+ def _read_string(text: str, line: int, column: int) -> tuple[str, int]:
118
+ assert text[0] == '"'
119
+ value_chars = []
120
+ i = 1
121
+ while i < len(text):
122
+ ch = text[i]
123
+ if ch == '"':
124
+ return "".join(value_chars), i + 1
125
+ value_chars.append(ch)
126
+ i += 1
127
+ raise Namel3ssError("Unterminated string literal", line=line, column=column)
128
+
129
+ @staticmethod
130
+ def _read_number(text: str) -> tuple[int, int]:
131
+ i = 0
132
+ digits = []
133
+ while i < len(text) and text[i].isdigit():
134
+ digits.append(text[i])
135
+ i += 1
136
+ return int("".join(digits)), i
137
+
138
+ @staticmethod
139
+ def _read_identifier(text: str) -> tuple[str, int]:
140
+ i = 0
141
+ chars = []
142
+ while i < len(text) and (text[i].isalnum() or text[i] == "_"):
143
+ chars.append(text[i])
144
+ i += 1
145
+ return "".join(chars), i
146
+
147
+ @staticmethod
148
+ def _keyword_value(token_type: str, raw: str):
149
+ if token_type == "BOOLEAN":
150
+ return raw.lower() == "true"
151
+ return raw
152
+
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ # Reserved words mapped to their token types. Keep each keyword string once to
7
+ # avoid silent overrides; duplicates hide earlier entries and break coverage.
8
+ # When adding new keywords, append here only after confirming the token type is
9
+ # unique and tests cover the new surface.
10
+ KEYWORDS = {
11
+ "flow": "FLOW",
12
+ "page": "PAGE",
13
+ "ai": "AI",
14
+ "ask": "ASK",
15
+ "with": "WITH",
16
+ "input": "INPUT",
17
+ "as": "AS",
18
+ "provider": "PROVIDER",
19
+ "tools": "TOOLS",
20
+ "expose": "EXPOSE",
21
+ "tool": "TOOL",
22
+ "kind": "KIND",
23
+ "memory": "MEMORY",
24
+ "short_term": "SHORT_TERM",
25
+ "semantic": "SEMANTIC",
26
+ "profile": "PROFILE",
27
+ "agent": "AGENT",
28
+ "agents": "AGENTS",
29
+ "parallel": "PARALLEL",
30
+ "run": "RUN",
31
+ "model": "MODEL",
32
+ "system_prompt": "SYSTEM_PROMPT",
33
+ "title": "TITLE",
34
+ "text": "TEXT",
35
+ "form": "FORM",
36
+ "table": "TABLE",
37
+ "button": "BUTTON",
38
+ "calls": "CALLS",
39
+ "record": "RECORD",
40
+ "save": "SAVE",
41
+ "find": "FIND",
42
+ "where": "WHERE",
43
+ "let": "LET",
44
+ "set": "SET",
45
+ "return": "RETURN",
46
+ "repeat": "REPEAT",
47
+ "up": "UP",
48
+ "to": "TO",
49
+ "times": "TIMES",
50
+ "for": "FOR",
51
+ "each": "EACH",
52
+ "in": "IN",
53
+ "match": "MATCH",
54
+ "when": "WHEN",
55
+ "otherwise": "OTHERWISE",
56
+ "try": "TRY",
57
+ "catch": "CATCH",
58
+ "if": "IF",
59
+ "else": "ELSE",
60
+ "is": "IS",
61
+ "greater": "GREATER",
62
+ "less": "LESS",
63
+ "equal": "EQUAL",
64
+ "than": "THAN",
65
+ "and": "AND",
66
+ "or": "OR",
67
+ "not": "NOT",
68
+ "state": "STATE",
69
+ "constant": "CONSTANT",
70
+ "true": "BOOLEAN",
71
+ "false": "BOOLEAN",
72
+ "string": "TYPE_STRING",
73
+ "int": "TYPE_INT",
74
+ "number": "TYPE_NUMBER",
75
+ "boolean": "TYPE_BOOLEAN",
76
+ "json": "TYPE_JSON",
77
+ "must": "MUST",
78
+ "be": "BE",
79
+ "present": "PRESENT",
80
+ "unique": "UNIQUE",
81
+ "pattern": "PATTERN",
82
+ "have": "HAVE",
83
+ "length": "LENGTH",
84
+ "at": "AT",
85
+ "least": "LEAST",
86
+ "most": "MOST",
87
+ }
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class Token:
92
+ type: str
93
+ value: Optional[object]
94
+ line: int
95
+ column: int
96
+
97
+ def __repr__(self) -> str: # pragma: no cover - debug helper
98
+ return f"Token({self.type}, {self.value}, {self.line}:{self.column})"
@@ -0,0 +1,4 @@
1
+ from namel3ss.lint.engine import lint_source
2
+ from namel3ss.lint.types import Finding
3
+
4
+ __all__ = ["lint_source", "Finding"]
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ from namel3ss.errors.base import Namel3ssError
4
+ from namel3ss.errors.render import format_error
5
+ from namel3ss.ir.nodes import lower_program
6
+ from namel3ss.lexer.tokens import KEYWORDS
7
+ from namel3ss.lint.semantic import lint_semantic
8
+ from namel3ss.lint.text_scan import scan_text
9
+ from namel3ss.lint.types import Finding
10
+ from namel3ss.parser.core import parse
11
+ from namel3ss.ast import nodes as ast
12
+
13
+
14
+ def lint_source(source: str) -> list[Finding]:
15
+ lines = source.splitlines()
16
+ findings = scan_text(lines)
17
+
18
+ ast_program = None
19
+ try:
20
+ ast_program = parse(source)
21
+ except Namel3ssError as err:
22
+ findings.append(
23
+ Finding(
24
+ code="lint.parse_failed",
25
+ message="Parse failed; showing best-effort lint results.",
26
+ line=err.line,
27
+ column=err.column,
28
+ severity="warning",
29
+ )
30
+ )
31
+ return findings
32
+
33
+ findings.extend(_lint_reserved_identifiers(ast_program))
34
+ flow_names = {flow.name for flow in ast_program.flows}
35
+ record_names = {record.name for record in ast_program.records}
36
+
37
+ try:
38
+ program_ir = lower_program(ast_program)
39
+ except Namel3ssError as err:
40
+ findings.extend(_lint_refs_ast(ast_program, flow_names, record_names))
41
+ findings.append(
42
+ Finding(
43
+ code="lint.parse_failed",
44
+ message="Lowering failed; showing best-effort lint results.",
45
+ line=err.line,
46
+ column=err.column,
47
+ severity="warning",
48
+ )
49
+ )
50
+ return findings
51
+
52
+ findings.extend(lint_semantic(program_ir))
53
+ return findings
54
+
55
+
56
+ def _lint_reserved_identifiers(ast_program) -> list[Finding]:
57
+ reserved = set(KEYWORDS.keys())
58
+ findings: list[Finding] = []
59
+
60
+ def walk_statements(stmts):
61
+ for stmt in stmts:
62
+ if hasattr(stmt, "body"):
63
+ walk_statements(getattr(stmt, "body"))
64
+ if hasattr(stmt, "then_body"):
65
+ walk_statements(stmt.then_body)
66
+ if hasattr(stmt, "else_body"):
67
+ walk_statements(stmt.else_body)
68
+ if hasattr(stmt, "try_body"):
69
+ walk_statements(stmt.try_body)
70
+ if hasattr(stmt, "catch_body"):
71
+ walk_statements(stmt.catch_body)
72
+ if hasattr(stmt, "cases"):
73
+ for case in stmt.cases:
74
+ walk_statements(case.body)
75
+ if stmt.__class__.__name__ == "Let":
76
+ if stmt.name in reserved:
77
+ findings.append(
78
+ Finding(
79
+ code="names.reserved_identifier",
80
+ message=f"Identifier '{stmt.name}' is reserved",
81
+ line=stmt.line,
82
+ column=stmt.column,
83
+ )
84
+ )
85
+
86
+ for flow in ast_program.flows:
87
+ walk_statements(flow.body)
88
+ return findings
89
+
90
+
91
+ def _lint_refs_ast(ast_program, flow_names: set[str], record_names: set[str]) -> list[Finding]:
92
+ findings: list[Finding] = []
93
+ for page in ast_program.pages:
94
+ for item in page.items:
95
+ if isinstance(item, ast.ButtonItem):
96
+ if item.flow_name not in flow_names:
97
+ findings.append(
98
+ Finding(
99
+ code="refs.unknown_flow",
100
+ message=f"Button references unknown flow '{item.flow_name}'",
101
+ line=item.line,
102
+ column=item.column,
103
+ )
104
+ )
105
+ if isinstance(item, ast.FormItem):
106
+ if item.record_name not in record_names:
107
+ findings.append(
108
+ Finding(
109
+ code="refs.unknown_record",
110
+ message=f"Form references unknown record '{item.record_name}'",
111
+ line=item.line,
112
+ column=item.column,
113
+ )
114
+ )
115
+ if isinstance(item, ast.TableItem):
116
+ if item.record_name not in record_names:
117
+ findings.append(
118
+ Finding(
119
+ code="refs.unknown_record",
120
+ message=f"Table references unknown record '{item.record_name}'",
121
+ line=item.line,
122
+ column=item.column,
123
+ )
124
+ )
125
+ return findings