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.
- namel3ss/__init__.py +4 -0
- namel3ss/ast/__init__.py +5 -0
- namel3ss/ast/agents.py +13 -0
- namel3ss/ast/ai.py +23 -0
- namel3ss/ast/base.py +10 -0
- namel3ss/ast/expressions.py +55 -0
- namel3ss/ast/nodes.py +86 -0
- namel3ss/ast/pages.py +43 -0
- namel3ss/ast/program.py +22 -0
- namel3ss/ast/records.py +27 -0
- namel3ss/ast/statements.py +107 -0
- namel3ss/ast/tool.py +11 -0
- namel3ss/cli/__init__.py +2 -0
- namel3ss/cli/actions_mode.py +39 -0
- namel3ss/cli/app_loader.py +22 -0
- namel3ss/cli/commands/action.py +27 -0
- namel3ss/cli/commands/run.py +43 -0
- namel3ss/cli/commands/ui.py +26 -0
- namel3ss/cli/commands/validate.py +23 -0
- namel3ss/cli/format_mode.py +30 -0
- namel3ss/cli/io/json_io.py +19 -0
- namel3ss/cli/io/read_source.py +16 -0
- namel3ss/cli/json_io.py +21 -0
- namel3ss/cli/lint_mode.py +29 -0
- namel3ss/cli/main.py +135 -0
- namel3ss/cli/new_mode.py +146 -0
- namel3ss/cli/runner.py +28 -0
- namel3ss/cli/studio_mode.py +22 -0
- namel3ss/cli/ui_mode.py +14 -0
- namel3ss/config/__init__.py +4 -0
- namel3ss/config/dotenv.py +33 -0
- namel3ss/config/loader.py +83 -0
- namel3ss/config/model.py +49 -0
- namel3ss/errors/__init__.py +2 -0
- namel3ss/errors/base.py +34 -0
- namel3ss/errors/render.py +22 -0
- namel3ss/format/__init__.py +3 -0
- namel3ss/format/formatter.py +18 -0
- namel3ss/format/rules.py +97 -0
- namel3ss/ir/__init__.py +3 -0
- namel3ss/ir/lowering/__init__.py +4 -0
- namel3ss/ir/lowering/agents.py +42 -0
- namel3ss/ir/lowering/ai.py +45 -0
- namel3ss/ir/lowering/expressions.py +49 -0
- namel3ss/ir/lowering/flow.py +21 -0
- namel3ss/ir/lowering/pages.py +48 -0
- namel3ss/ir/lowering/program.py +34 -0
- namel3ss/ir/lowering/records.py +25 -0
- namel3ss/ir/lowering/statements.py +122 -0
- namel3ss/ir/lowering/tools.py +16 -0
- namel3ss/ir/model/__init__.py +50 -0
- namel3ss/ir/model/agents.py +33 -0
- namel3ss/ir/model/ai.py +31 -0
- namel3ss/ir/model/base.py +20 -0
- namel3ss/ir/model/expressions.py +50 -0
- namel3ss/ir/model/pages.py +43 -0
- namel3ss/ir/model/program.py +28 -0
- namel3ss/ir/model/statements.py +76 -0
- namel3ss/ir/model/tools.py +11 -0
- namel3ss/ir/nodes.py +88 -0
- namel3ss/lexer/__init__.py +2 -0
- namel3ss/lexer/lexer.py +152 -0
- namel3ss/lexer/tokens.py +98 -0
- namel3ss/lint/__init__.py +4 -0
- namel3ss/lint/engine.py +125 -0
- namel3ss/lint/semantic.py +45 -0
- namel3ss/lint/text_scan.py +70 -0
- namel3ss/lint/types.py +22 -0
- namel3ss/parser/__init__.py +3 -0
- namel3ss/parser/agent.py +78 -0
- namel3ss/parser/ai.py +113 -0
- namel3ss/parser/constraints.py +37 -0
- namel3ss/parser/core.py +166 -0
- namel3ss/parser/expressions.py +105 -0
- namel3ss/parser/flow.py +37 -0
- namel3ss/parser/pages.py +76 -0
- namel3ss/parser/program.py +45 -0
- namel3ss/parser/records.py +66 -0
- namel3ss/parser/statements/__init__.py +27 -0
- namel3ss/parser/statements/control_flow.py +116 -0
- namel3ss/parser/statements/core.py +66 -0
- namel3ss/parser/statements/data.py +17 -0
- namel3ss/parser/statements/letset.py +22 -0
- namel3ss/parser/statements.py +1 -0
- namel3ss/parser/tokens.py +35 -0
- namel3ss/parser/tool.py +29 -0
- namel3ss/runtime/__init__.py +3 -0
- namel3ss/runtime/ai/http/client.py +24 -0
- namel3ss/runtime/ai/mock_provider.py +5 -0
- namel3ss/runtime/ai/provider.py +29 -0
- namel3ss/runtime/ai/providers/__init__.py +18 -0
- namel3ss/runtime/ai/providers/_shared/errors.py +20 -0
- namel3ss/runtime/ai/providers/_shared/parse.py +18 -0
- namel3ss/runtime/ai/providers/anthropic.py +55 -0
- namel3ss/runtime/ai/providers/gemini.py +50 -0
- namel3ss/runtime/ai/providers/mistral.py +51 -0
- namel3ss/runtime/ai/providers/mock.py +23 -0
- namel3ss/runtime/ai/providers/ollama.py +39 -0
- namel3ss/runtime/ai/providers/openai.py +55 -0
- namel3ss/runtime/ai/providers/registry.py +38 -0
- namel3ss/runtime/ai/trace.py +18 -0
- namel3ss/runtime/executor/__init__.py +3 -0
- namel3ss/runtime/executor/agents.py +91 -0
- namel3ss/runtime/executor/ai_runner.py +90 -0
- namel3ss/runtime/executor/api.py +54 -0
- namel3ss/runtime/executor/assign.py +40 -0
- namel3ss/runtime/executor/context.py +31 -0
- namel3ss/runtime/executor/executor.py +77 -0
- namel3ss/runtime/executor/expr_eval.py +110 -0
- namel3ss/runtime/executor/records_ops.py +64 -0
- namel3ss/runtime/executor/result.py +13 -0
- namel3ss/runtime/executor/signals.py +6 -0
- namel3ss/runtime/executor/statements.py +99 -0
- namel3ss/runtime/memory/manager.py +52 -0
- namel3ss/runtime/memory/profile.py +17 -0
- namel3ss/runtime/memory/semantic.py +20 -0
- namel3ss/runtime/memory/short_term.py +18 -0
- namel3ss/runtime/records/service.py +105 -0
- namel3ss/runtime/store/__init__.py +2 -0
- namel3ss/runtime/store/memory_store.py +62 -0
- namel3ss/runtime/tools/registry.py +13 -0
- namel3ss/runtime/ui/__init__.py +2 -0
- namel3ss/runtime/ui/actions.py +124 -0
- namel3ss/runtime/validators/__init__.py +2 -0
- namel3ss/runtime/validators/constraints.py +126 -0
- namel3ss/schema/__init__.py +2 -0
- namel3ss/schema/records.py +52 -0
- namel3ss/studio/__init__.py +4 -0
- namel3ss/studio/api.py +115 -0
- namel3ss/studio/edit/__init__.py +3 -0
- namel3ss/studio/edit/ops.py +80 -0
- namel3ss/studio/edit/selectors.py +74 -0
- namel3ss/studio/edit/transform.py +39 -0
- namel3ss/studio/server.py +175 -0
- namel3ss/studio/session.py +11 -0
- namel3ss/studio/web/app.js +248 -0
- namel3ss/studio/web/index.html +44 -0
- namel3ss/studio/web/styles.css +42 -0
- namel3ss/templates/__init__.py +3 -0
- namel3ss/templates/__pycache__/__init__.cpython-312.pyc +0 -0
- namel3ss/templates/ai_assistant/.gitignore +1 -0
- namel3ss/templates/ai_assistant/README.md +10 -0
- namel3ss/templates/ai_assistant/app.ai +30 -0
- namel3ss/templates/crud/.gitignore +1 -0
- namel3ss/templates/crud/README.md +10 -0
- namel3ss/templates/crud/app.ai +26 -0
- namel3ss/templates/multi_agent/.gitignore +1 -0
- namel3ss/templates/multi_agent/README.md +10 -0
- namel3ss/templates/multi_agent/app.ai +43 -0
- namel3ss/ui/__init__.py +2 -0
- namel3ss/ui/manifest.py +220 -0
- namel3ss/utils/__init__.py +2 -0
- namel3ss-0.1.0a0.dist-info/METADATA +123 -0
- namel3ss-0.1.0a0.dist-info/RECORD +157 -0
- namel3ss-0.1.0a0.dist-info/WHEEL +5 -0
- namel3ss-0.1.0a0.dist-info/entry_points.txt +2 -0
- 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
|
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
|
+
]
|
namel3ss/lexer/lexer.py
ADDED
|
@@ -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
|
+
|
namel3ss/lexer/tokens.py
ADDED
|
@@ -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})"
|
namel3ss/lint/engine.py
ADDED
|
@@ -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
|