ndnc 0.0.1__tar.gz

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.
ndnc-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tryuuu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ndnc-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: ndnc
3
+ Version: 0.0.1
4
+ Summary: A DSL interpreter for transparent distributed execution over NDN
5
+ Author-email: tryuuu <ryu23210@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/tryuuu/ndn-compiler
8
+ Project-URL: Repository, https://github.com/tryuuu/ndn-compiler
9
+ Keywords: ndn,dsl,distributed,icn
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Interpreters
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: lark==1.1.9
19
+ Requires-Dist: python-ndn
20
+ Dynamic: license-file
21
+
22
+ # Description
23
+ A minimal domain-specific language (DSL) interpreter for NDN-less syntax.
24
+ Currently supports several simple operations.
25
+ # Setup
26
+ ## Start Environment (Docker)
27
+ Build and start NFD and Producer containers.
28
+ ```bash
29
+ make all
30
+ ```
31
+ ## Run examples
32
+ Run the consumer in a container.
33
+ ```bash
34
+ make run
35
+ ```
36
+
37
+ ローカル関数(`modify`)の動作確認:
38
+ ```bash
39
+ make run S=examples/hello.ndn
40
+ # 出力例: local data from function
41
+ ```
42
+
43
+ リモート関数(`remote_modify`)の動作確認:
44
+ ```bash
45
+ make run S=examples/remote.ndn
46
+ # 出力例: local data from remote_modify
47
+ ```
48
+
49
+ `remote_modify` は NDN Interest `/remote_modify/<arg>` を発行し、`remote_modify` コンテナが処理を行う。`make all` 実行時に自動で起動する。
50
+ ## Check Logs
51
+ ```bash
52
+ make logs
53
+ ```
54
+ ## Stop Environment
55
+ ```bash
56
+ make down
57
+ ```
ndnc-0.0.1/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Description
2
+ A minimal domain-specific language (DSL) interpreter for NDN-less syntax.
3
+ Currently supports several simple operations.
4
+ # Setup
5
+ ## Start Environment (Docker)
6
+ Build and start NFD and Producer containers.
7
+ ```bash
8
+ make all
9
+ ```
10
+ ## Run examples
11
+ Run the consumer in a container.
12
+ ```bash
13
+ make run
14
+ ```
15
+
16
+ ローカル関数(`modify`)の動作確認:
17
+ ```bash
18
+ make run S=examples/hello.ndn
19
+ # 出力例: local data from function
20
+ ```
21
+
22
+ リモート関数(`remote_modify`)の動作確認:
23
+ ```bash
24
+ make run S=examples/remote.ndn
25
+ # 出力例: local data from remote_modify
26
+ ```
27
+
28
+ `remote_modify` は NDN Interest `/remote_modify/<arg>` を発行し、`remote_modify` コンテナが処理を行う。`make all` 実行時に自動で起動する。
29
+ ## Check Logs
30
+ ```bash
31
+ make logs
32
+ ```
33
+ ## Stop Environment
34
+ ```bash
35
+ make down
36
+ ```
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+
6
+ [project]
7
+ name = "ndnc"
8
+ version = "0.0.1"
9
+ description = "A DSL interpreter for transparent distributed execution over NDN"
10
+ authors = [{ name = "tryuuu", email = "ryu23210@gmail.com" }]
11
+ readme = "README.md"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ requires-python = ">=3.10"
15
+ dependencies = [
16
+ "lark==1.1.9",
17
+ "python-ndn"
18
+ ]
19
+ keywords = ["ndn", "dsl", "distributed", "icn"]
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Software Development :: Interpreters",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/tryuuu/ndn-compiler"
30
+ Repository = "https://github.com/tryuuu/ndn-compiler"
31
+
32
+ [project.scripts]
33
+ ndnc = "ndnc.cli:main"
34
+
35
+ [tool.setuptools.package-data]
36
+ "ndnc.parser" = ["*.lark"]
ndnc-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+ import argparse
3
+ from pathlib import Path
4
+ from .parser.parser import parse
5
+ from .interp.evaluator import Interpreter
6
+ from .server import Server
7
+
8
+ def main():
9
+ ap = argparse.ArgumentParser(prog="ndnc", description="NDN-less minimal DSL interpreter (print only)")
10
+ sub = ap.add_subparsers(dest="cmd", required=True)
11
+
12
+ ap_run = sub.add_parser("run", help="Interpret and run a .ndn file")
13
+ ap_run.add_argument("source", type=Path)
14
+ ap_run.add_argument("ndn_args", nargs="*", metavar="ARG",
15
+ help="NDN names passed as arg0, arg1, ... to the script")
16
+
17
+ ap_serve = sub.add_parser("serve", help="Start NDN server (producer)")
18
+
19
+ args = ap.parse_args()
20
+
21
+ if args.cmd == "run":
22
+ code = args.source.read_text(encoding="utf-8")
23
+ prog = parse(code)
24
+ # 位置引数を arg0, arg1, ... として Interpreter に渡す
25
+ ndn_args = {f"arg{i}": v for i, v in enumerate(args.ndn_args)}
26
+ Interpreter(args=ndn_args).run(prog)
27
+ elif args.cmd == "serve":
28
+ Server().run()
29
+
30
+
31
+ if __name__ == "__main__":
32
+ main()
File without changes
@@ -0,0 +1,255 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Union, Optional
3
+ import asyncio
4
+ import contextlib
5
+ import io
6
+ import traceback
7
+ import sys
8
+ from ndn.app import NDNApp
9
+ from ndn.encoding import Name
10
+ from ndn.security import KeychainDigest
11
+ from ..parser.ast import (
12
+ Program, PrintStatement, Assignment, ExprStatement,
13
+ StringLiteral, NumberLiteral, Variable,
14
+ ExpressInterest, FunctionCall, Expr
15
+ )
16
+
17
+ # ローカルで処理できる関数名のセット
18
+ _LOCAL_FUNCTIONS = {"modify", "concat"}
19
+
20
+ class Interpreter:
21
+ def __init__(self, args: dict[str, str] | None = None):
22
+ self._env: dict[str, Any] = {}
23
+ self._env_origin: dict[str, str] = {} # interest で取得した変数の NDN 名を追跡
24
+ # 外部から渡された引数を env に事前登録(例: {"arg0": "/data/ryu-local/"})
25
+ if args:
26
+ self._env.update(args)
27
+ self.app: Optional[NDNApp] = None
28
+ self._local_data: dict[str, str] = {
29
+ '/data/ryu-local/': 'local data',
30
+ '/height/Mt.Fuji/': '3776m',
31
+ }
32
+
33
+ def run(self, program: Program):
34
+ has_interest = any(
35
+ (isinstance(st, ExprStatement) and self._has_interest(st.expr)) or
36
+ (isinstance(st, PrintStatement) and self._has_interest(st.expr)) or
37
+ (isinstance(st, Assignment) and self._has_interest(st.expr))
38
+ for st in program
39
+ )
40
+
41
+ if has_interest:
42
+ try:
43
+ self.app = NDNApp(keychain=KeychainDigest())
44
+ # ローカルデータを NDN プロデューサーとして登録する
45
+ # (リモート関数がこれらをフェッチできるようにするため)
46
+ self._register_local_data_routes()
47
+
48
+ async def after_start():
49
+ try:
50
+ await self._exec_block(program)
51
+ except Exception:
52
+ traceback.print_exc()
53
+ raise
54
+ finally:
55
+ self.app.shutdown()
56
+
57
+ self.app.run_forever(after_start=after_start())
58
+
59
+ except Exception as e:
60
+ print(f"DEBUG: NDN Connection/Execution Failed: {e}")
61
+ traceback.print_exc()
62
+ self.app = None
63
+ asyncio.run(self._exec_block(program))
64
+ else:
65
+ asyncio.run(self._exec_block(program))
66
+
67
+ def _has_interest(self, expr: Expr) -> bool:
68
+ if isinstance(expr, ExpressInterest):
69
+ if expr.name_is_var:
70
+ # 変数の値は実行時まで不明なのでネットワーク必要とみなす
71
+ return True
72
+ # _local_data にあればネットワーク不要
73
+ return expr.name not in self._local_data
74
+ if isinstance(expr, Variable):
75
+ return False
76
+ if isinstance(expr, FunctionCall):
77
+ # ローカルにない関数はリモート呼び出しになるため NDNApp が必要
78
+ return (expr.name not in _LOCAL_FUNCTIONS) or any(self._has_interest(a) for a in expr.args)
79
+ return False
80
+
81
+ async def _exec_block(self, node: Program):
82
+ for st in node:
83
+ if isinstance(st, PrintStatement):
84
+ await self._exec_print(st)
85
+ elif isinstance(st, Assignment):
86
+ await self._exec_assignment(st)
87
+ elif isinstance(st, ExprStatement):
88
+ await self._exec_expr_stmt(st)
89
+ else:
90
+ raise RuntimeError(f"Unsupported node: {st}")
91
+
92
+ async def _exec_print(self, node: PrintStatement):
93
+ value = await self._eval_expr(node.expr)
94
+ print(value)
95
+
96
+ async def _exec_assignment(self, node: Assignment):
97
+ value = await self._eval_expr(node.expr)
98
+ self._env[node.name] = value
99
+ # interest で取得した変数は NDN 名を記録しておく
100
+ if isinstance(node.expr, ExpressInterest):
101
+ if node.expr.name_is_var:
102
+ # 変数名から実際の NDN 名を解決して記録
103
+ ndn_name = self._env.get(node.expr.name, "")
104
+ self._env_origin[node.name] = str(ndn_name)
105
+ else:
106
+ self._env_origin[node.name] = node.expr.name
107
+
108
+ async def _exec_expr_stmt(self, node: ExprStatement):
109
+ value = await self._eval_expr(node.expr)
110
+ print(value)
111
+
112
+ async def _eval_expr(self, expr: Expr) -> Union[int, str]:
113
+ if isinstance(expr, StringLiteral):
114
+ return expr.value
115
+
116
+ if isinstance(expr, NumberLiteral):
117
+ return expr.value
118
+
119
+ if isinstance(expr, Variable):
120
+ if expr.name not in self._env:
121
+ raise RuntimeError(f"Variable '{expr.name}' is not defined")
122
+ return self._env[expr.name]
123
+
124
+ if isinstance(expr, ExpressInterest):
125
+ # name_is_var のとき、変数から実際の NDN 名を解決する
126
+ if expr.name_is_var:
127
+ if expr.name not in self._env:
128
+ raise RuntimeError(f"Variable '{expr.name}' is not defined (used in interest)")
129
+ ndn_name = str(self._env[expr.name])
130
+ else:
131
+ ndn_name = expr.name
132
+
133
+ if not ndn_name.endswith('/'):
134
+ print(f"Error: Interest name must end with a trailing slash. Got: {ndn_name}", file=sys.stderr)
135
+ print(f"Expected: {ndn_name}/", file=sys.stderr)
136
+ sys.exit(1)
137
+
138
+ if ndn_name in self._local_data:
139
+ local_value = self._local_data[ndn_name]
140
+ try:
141
+ return int(local_value)
142
+ except ValueError:
143
+ return local_value
144
+
145
+ if self.app is None:
146
+ return f"mock_{ndn_name.replace('/', '_')}"
147
+
148
+ try:
149
+ _, _, content = await self.app.express_interest(
150
+ ndn_name,
151
+ must_be_fresh=True,
152
+ can_be_prefix=True,
153
+ lifetime=6000
154
+ )
155
+ if content is None:
156
+ return ""
157
+ text = bytes(content).decode('utf-8').strip()
158
+ try:
159
+ return int(text)
160
+ except ValueError:
161
+ return text
162
+ except Exception as e:
163
+ print(f"Error expressing interest for {expr.name}: {e}")
164
+ raise e
165
+
166
+ if isinstance(expr, FunctionCall):
167
+ if expr.name in _LOCAL_FUNCTIONS:
168
+ arg_values = [await self._eval_expr(a) for a in expr.args]
169
+ if expr.name == "m_to_feet":
170
+ meters_str = str(arg_values[0]).rstrip('m')
171
+ feet = round(float(meters_str) * 3.28084)
172
+ return f"{feet}ft"
173
+ if expr.name == "concat":
174
+ return "".join(str(v) for v in arg_values)
175
+ return str(arg_values[0]) + " from function"
176
+ elif self.app is not None:
177
+ # リモート関数: 引数を NDN 名として渡す(ネストした関数呼び出しも再帰的に解決)
178
+ ndn_names = [self._to_ndn_name(a) for a in expr.args]
179
+ return await self._call_remote_function(expr.name, ndn_names)
180
+ else:
181
+ raise RuntimeError(f"Unknown function: {expr.name}")
182
+
183
+ raise RuntimeError(f"Unsupported expr: {expr}")
184
+
185
+ def _register_local_data_routes(self):
186
+ """ローカルデータを NDN プロデューサーとして登録する。
187
+ リモート関数がフェッチできるよう、Interest に応答できるようにする。"""
188
+ for ndn_name, value in self._local_data.items():
189
+ prefix = ndn_name.rstrip('/')
190
+ val_bytes = str(value).encode()
191
+
192
+ def make_handler(content):
193
+ def handler(name, param, app_param):
194
+ self.app.put_data(name, content=content, freshness_period=10000)
195
+ return handler
196
+
197
+ self.app.route(prefix)(make_handler(val_bytes))
198
+
199
+ def _to_ndn_name(self, expr: Expr) -> str:
200
+ """リモート関数の引数として使う NDN 名を決定する。
201
+ - ExpressInterest → そのまま NDN 名を返す
202
+ - Variable → interest 由来なら記録済みの NDN 名、そうでなければ値を NDN 名として扱う
203
+ - StringLiteral → 先頭 '/' を補完して NDN 名とする"""
204
+ if isinstance(expr, ExpressInterest):
205
+ if expr.name_is_var:
206
+ if expr.name in self._env_origin:
207
+ return self._env_origin[expr.name]
208
+ val = self._env.get(expr.name, "")
209
+ return str(val) if str(val).startswith('/') else '/' + str(val)
210
+ return expr.name
211
+ if isinstance(expr, Variable):
212
+ if expr.name in self._env_origin:
213
+ return self._env_origin[expr.name]
214
+ val = self._env.get(expr.name, "")
215
+ if isinstance(val, str):
216
+ return val if val.startswith('/') else '/' + val
217
+ return str(val)
218
+ if isinstance(expr, StringLiteral):
219
+ val = expr.value
220
+ return val if val.startswith('/') else '/' + val
221
+ if isinstance(expr, FunctionCall):
222
+ if expr.name not in _LOCAL_FUNCTIONS:
223
+ ndn_names = [self._to_ndn_name(a) for a in expr.args]
224
+ args_str = ", ".join(ndn_names)
225
+ return "/" + expr.name + "/(" + args_str + ")"
226
+ return str(expr)
227
+
228
+ async def _call_remote_function(self, func_name: str, ndn_names: list[str]) -> str:
229
+ # Sidecar に倣い、括弧記法で Interest 名を構築する
230
+ # 例: /temperature_average/(/data/tokyo, /data/paris)
231
+ args_str = ", ".join(ndn_names)
232
+ interest_name = "/" + func_name + "/(" + args_str + ")"
233
+ try:
234
+ _, _, content = await self.app.express_interest(
235
+ interest_name,
236
+ must_be_fresh=True,
237
+ can_be_prefix=False,
238
+ lifetime=20000 # リモート関数が引数をフェッチする時間を考慮して長めに設定
239
+ )
240
+ if content is None:
241
+ return ""
242
+ return bytes(content).decode('utf-8').strip()
243
+ except Exception as e:
244
+ print(f"Error calling remote function '{func_name}': {e}")
245
+ raise
246
+
247
+ async def exec_in_context(self, program: Program, app: NDNApp) -> str:
248
+ """既存の NDNApp のコンテキスト内で .ndn プログラムを実行し、出力を文字列で返す。
249
+ seed サーバーなど、すでにイベントループが動いている環境から呼び出す用途向け。
250
+ 通常の run() と異なり、新たなイベントループや NDNApp を起動しない。"""
251
+ self.app = app
252
+ buffer = io.StringIO()
253
+ with contextlib.redirect_stdout(buffer):
254
+ await self._exec_block(program)
255
+ return buffer.getvalue().strip()
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List, Union
5
+
6
+
7
+ @dataclass
8
+ class PrintStatement:
9
+ expr: "Expr"
10
+
11
+ @dataclass
12
+ class Assignment:
13
+ name: str
14
+ expr: "Expr"
15
+
16
+ @dataclass
17
+ class StringLiteral:
18
+ value: str
19
+
20
+ @dataclass
21
+ class NumberLiteral:
22
+ value: int
23
+
24
+ @dataclass
25
+ class Variable:
26
+ name: str
27
+
28
+ @dataclass
29
+ class ExpressInterest:
30
+ name: str
31
+ name_is_var: bool = False # True のとき name は変数名(実行時に env から解決する)
32
+
33
+ @dataclass
34
+ class FunctionCall:
35
+ name: str
36
+ args: List["Expr"]
37
+
38
+ Expr = Union[StringLiteral, NumberLiteral, Variable, ExpressInterest, FunctionCall]
39
+
40
+ @dataclass
41
+ class ExprStatement:
42
+ expr: Expr
43
+
44
+
45
+ Statement = Union[PrintStatement, Assignment, ExprStatement]
46
+ Program = List[Statement]
@@ -0,0 +1,26 @@
1
+ // Simple grammar: only print statements of string literals
2
+
3
+ start: statement+
4
+
5
+ // 定義できる文法
6
+ ?statement: LET IDENTIFIER "=" expr -> assignment_stmt
7
+ | PRINT expr -> print_stmt
8
+ | expr -> expr_stmt
9
+
10
+ // exprの種類を定義
11
+ ?expr: STRING -> string_literal
12
+ | NUMBER -> number_literal
13
+ | IDENTIFIER "(" expr ("," expr)* ")" -> call_expr
14
+ | IDENTIFIER -> variable
15
+ | INTEREST STRING -> interest_expr
16
+ | INTEREST IDENTIFIER -> interest_var_expr
17
+
18
+ LET: "let"
19
+ PRINT: "print"
20
+ INTEREST: "interest"
21
+ IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
22
+
23
+ %import common.ESCAPED_STRING -> STRING
24
+ %import common.SIGNED_NUMBER -> NUMBER
25
+ %import common.WS
26
+ %ignore WS
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ from lark import Lark, Transformer, v_args
7
+
8
+ from .ast import (
9
+ PrintStatement, Assignment, ExprStatement,
10
+ StringLiteral, NumberLiteral, Variable,
11
+ ExpressInterest, FunctionCall, Program, Expr
12
+ )
13
+
14
+
15
+ def _load_grammar() -> str:
16
+ grammar_path = Path(__file__).with_name("grammar.lark")
17
+ return grammar_path.read_text(encoding="utf-8")
18
+
19
+
20
+ _PARSER = Lark(_load_grammar(), start="start", parser="lalr")
21
+
22
+
23
+ class _BuildAST(Transformer):
24
+ @v_args(inline=True)
25
+ def assignment_stmt(self, let_token, identifier_token, expr: Expr): # type: ignore[override]
26
+ name = str(identifier_token)
27
+ return Assignment(name=name, expr=expr)
28
+
29
+ @v_args(inline=True)
30
+ def print_stmt(self, print_token, expr: Expr): # type: ignore[override]
31
+ return PrintStatement(expr=expr)
32
+
33
+ @v_args(inline=True)
34
+ def expr_stmt(self, expr: Expr): # type: ignore[override]
35
+ return ExprStatement(expr=expr)
36
+
37
+ @v_args(inline=True)
38
+ def string_literal(self, string_token): # type: ignore[override]
39
+ text = str(string_token)
40
+ if (text.startswith('"') and text.endswith('"')) or (text.startswith("'") and text.endswith("'")):
41
+ text = text[1:-1]
42
+ return StringLiteral(value=text)
43
+
44
+ @v_args(inline=True)
45
+ def number_literal(self, num_token): # type: ignore[override]
46
+ return NumberLiteral(value=int(str(num_token)))
47
+
48
+ @v_args(inline=True)
49
+ def variable(self, identifier_token): # type: ignore[override]
50
+ name = str(identifier_token)
51
+ return Variable(name=name)
52
+
53
+ @v_args(inline=True)
54
+ def interest_expr(self, interest_token, string_token): # type: ignore[override]
55
+ text = str(string_token)
56
+ if (text.startswith('"') and text.endswith('"')) or (text.startswith("'") and text.endswith("'")):
57
+ text = text[1:-1]
58
+ return ExpressInterest(name=text)
59
+
60
+ @v_args(inline=True)
61
+ def interest_var_expr(self, interest_token, identifier_token): # type: ignore[override]
62
+ return ExpressInterest(name=str(identifier_token), name_is_var=True)
63
+
64
+ def call_expr(self, items): # type: ignore[override]
65
+ name = str(items[0])
66
+ args = list(items[1:])
67
+ return FunctionCall(name=name, args=args)
68
+
69
+ def start(self, stmts): # type: ignore[override]
70
+ if isinstance(stmts, list):
71
+ return stmts
72
+ else:
73
+ return [stmts]
74
+
75
+ def parse(source: str) -> Program:
76
+ tree = _PARSER.parse(source)
77
+ program: Program = _BuildAST().transform(tree)
78
+ return program
@@ -0,0 +1,222 @@
1
+ import asyncio
2
+ import urllib.parse
3
+ from typing import Optional
4
+ from ndn.app import NDNApp
5
+ from ndn.encoding import Name, InterestParam, BinaryStr, FormalName
6
+ from ndn.security import KeychainDigest
7
+ from ndn.types import InterestNack, InterestTimeout
8
+
9
+ import logging
10
+ logging.basicConfig(format='[{asctime}]{levelname}:{message}',
11
+ datefmt='%Y-%m-%d %H:%M:%S',
12
+ level=logging.INFO,
13
+ style='{')
14
+
15
+ _TEMPERATURE_DATA: dict[str, float] = {
16
+ '/data/tokyo': 22.5,
17
+ '/data/paris': 18.0,
18
+ '/data/newyork': 15.3,
19
+ '/data/london': 12.8,
20
+ '/data/sydney': 26.1,
21
+ }
22
+
23
+ app = NDNApp(keychain=KeychainDigest())
24
+
25
+
26
+ def decode_and_remove_metadata(name: FormalName) -> str:
27
+ """NDN FormalName をデコードし、/t= などのメタデータを除去して返す。"""
28
+ decoded = Name.to_str(name)
29
+ decoded = urllib.parse.unquote(decoded)
30
+ # ')' の直後に '/' が続く場合はそこで打ち切る
31
+ end = decoded.rfind(')')
32
+ if end != -1 and len(decoded) > end + 1 and decoded[end + 1] == '/':
33
+ decoded = decoded[:end + 1]
34
+ # /t= メタデータを除去
35
+ t_idx = decoded.rfind('/t=')
36
+ if t_idx != -1:
37
+ decoded = decoded[:t_idx]
38
+ return decoded
39
+
40
+
41
+ def is_function_request(name: FormalName) -> bool:
42
+ """/( が含まれていれば関数リクエストと判定する。"""
43
+ return "/(" in decode_and_remove_metadata(name)
44
+
45
+
46
+ def extract_first_level_args(name: FormalName) -> list[str]:
47
+ """関数 Interest から第一階層の引数(NDN 名)リストを取り出す。
48
+ ネストした括弧にも対応。例: /f/(/a, /g/(/b)) → ['/a', '/g/(/b)']"""
49
+ decoded = decode_and_remove_metadata(name)
50
+ if '(' not in decoded:
51
+ return [decoded.strip()]
52
+
53
+ start_of_args = decoded.find('/(') + 2 # '(' の直後
54
+ args_str = decoded[start_of_args:-1] # 末尾の ')' を除く
55
+
56
+ args: list[str] = []
57
+ start = 0
58
+ depth = 0
59
+ for i, ch in enumerate(args_str):
60
+ if ch == '(':
61
+ depth += 1
62
+ elif ch == ')':
63
+ depth -= 1
64
+ elif ch == ',' and depth == 0:
65
+ args.append(args_str[start:i].strip())
66
+ start = i + 1
67
+ args.append(args_str[start:].strip())
68
+ return args
69
+
70
+
71
+ async def _fetch_arg(ndn_name: str) -> Optional[bytes]:
72
+ """引数の NDN 名を解決する。
73
+ ローカルデータストアを先に確認し、なければ consumer_app で Interest を発行する。
74
+ 引数がネストした関数呼び出しの場合も再帰的に解決される。"""
75
+ key = ndn_name.rstrip('/')
76
+ if key in _TEMPERATURE_DATA:
77
+ logging.info(f"[local] {key} = {_TEMPERATURE_DATA[key]}")
78
+ return str(_TEMPERATURE_DATA[key]).encode()
79
+
80
+ # NDN ネットワーク経由で取得(ネストした関数呼び出しを含む)
81
+ logging.info(f"[fetch] {ndn_name}")
82
+ for attempt in range(3):
83
+ try:
84
+ _, _, content = await app.express_interest(
85
+ ndn_name, must_be_fresh=True, can_be_prefix=False, lifetime=10000
86
+ )
87
+ if content:
88
+ logging.info(f"[fetch] OK {ndn_name} (attempt {attempt + 1})")
89
+ return bytes(content)
90
+ except (InterestNack, InterestTimeout) as e:
91
+ logging.warning(f"[fetch] {ndn_name} attempt {attempt + 1} failed: {e}")
92
+ await asyncio.sleep(0.3)
93
+ logging.error(f"[fetch] GIVE-UP {ndn_name}")
94
+ return None
95
+
96
+
97
+ @app.route('/data')
98
+ def on_data(name: FormalName, param: InterestParam, _app_param: Optional[BinaryStr]):
99
+ """データリクエストに応答する(/data/* プレフィックス)。"""
100
+ name_str = Name.to_str(name)
101
+ t_idx = name_str.rfind('/t=')
102
+ if t_idx != -1:
103
+ name_str = name_str[:t_idx]
104
+ key = name_str.rstrip('/')
105
+ if key in _TEMPERATURE_DATA:
106
+ logging.info(f"Serving data: {key}")
107
+ app.put_data(name, content=str(_TEMPERATURE_DATA[key]).encode(), freshness_period=10000)
108
+ else:
109
+ logging.warning(f"Unknown data key: {key}")
110
+
111
+
112
+ @app.route('/remote_modify')
113
+ def on_modify(name: FormalName, param: InterestParam, _app_param: Optional[BinaryStr]):
114
+ async def handler():
115
+ logging.info(f"Interest: {Name.to_str(name)}")
116
+ if not is_function_request(name):
117
+ logging.warning("Not a function request")
118
+ return
119
+
120
+ args = extract_first_level_args(name)
121
+ logging.info(f"Args: {args}")
122
+
123
+ contents = await asyncio.gather(*[_fetch_arg(a) for a in args])
124
+ if any(c is None for c in contents):
125
+ app.put_data(name, content=b"error: failed to fetch argument", freshness_period=10000)
126
+ return
127
+
128
+ result = f"{contents[0].decode()} from remote_modify"
129
+ logging.info(f"Result: {result!r}")
130
+ app.put_data(name, content=result.encode(), freshness_period=10000)
131
+
132
+ asyncio.create_task(handler())
133
+
134
+
135
+ @app.route('/temperature_average')
136
+ def on_temperature_average(name: FormalName, param: InterestParam, _app_param: Optional[BinaryStr]):
137
+ async def handler():
138
+ logging.info(f"Interest: {Name.to_str(name)}")
139
+ if not is_function_request(name):
140
+ logging.warning("Not a function request")
141
+ return
142
+
143
+ args = extract_first_level_args(name)
144
+ logging.info(f"Args: {args}")
145
+
146
+ contents = await asyncio.gather(*[_fetch_arg(a) for a in args])
147
+ if any(c is None for c in contents):
148
+ app.put_data(name, content=b"error: failed to fetch argument(s)", freshness_period=10000)
149
+ return
150
+
151
+ try:
152
+ temps = [float(c.decode()) for c in contents]
153
+ average = sum(temps) / len(temps)
154
+ result = f"{average:.1f}"
155
+ logging.info(f"temperature_average{tuple(args)} = {result}")
156
+ app.put_data(name, content=result.encode(), freshness_period=10000)
157
+ except ValueError as e:
158
+ app.put_data(name, content=f"error: {e}".encode(), freshness_period=10000)
159
+
160
+ asyncio.create_task(handler())
161
+
162
+
163
+ @app.route('/format_temp')
164
+ def on_format_temp(name: FormalName, param: InterestParam, _app_param: Optional[BinaryStr]):
165
+ async def handler():
166
+ logging.info(f"Interest: {Name.to_str(name)}")
167
+ if not is_function_request(name):
168
+ logging.warning("Not a function request")
169
+ return
170
+
171
+ args = extract_first_level_args(name)
172
+ logging.info(f"Args: {args}")
173
+
174
+ contents = await asyncio.gather(*[_fetch_arg(a) for a in args])
175
+ if any(c is None for c in contents):
176
+ app.put_data(name, content=b"error: failed to fetch argument", freshness_period=10000)
177
+ return
178
+
179
+ result = f"{contents[0].decode()}°C"
180
+ logging.info(f"Result: {result!r}")
181
+ app.put_data(name, content=result.encode(), freshness_period=10000)
182
+
183
+ asyncio.create_task(handler())
184
+
185
+
186
+ @app.route('/m_to_feet')
187
+ def on_m_to_feet(name: FormalName, param: InterestParam, _app_param: Optional[BinaryStr]):
188
+ async def handler():
189
+ logging.info(f"Interest: {Name.to_str(name)}")
190
+ if not is_function_request(name):
191
+ logging.warning("Not a function request")
192
+ return
193
+
194
+ args = extract_first_level_args(name)
195
+ logging.info(f"Args: {args}")
196
+
197
+ contents = await asyncio.gather(*[_fetch_arg(a) for a in args])
198
+ if any(c is None for c in contents):
199
+ app.put_data(name, content=b"error: failed to fetch argument", freshness_period=10000)
200
+ return
201
+
202
+ try:
203
+ meters_str = contents[0].decode().rstrip('m')
204
+ feet = round(float(meters_str) * 3.28084)
205
+ result = f"{feet}ft"
206
+ logging.info(f"Result: {result!r}")
207
+ app.put_data(name, content=result.encode(), freshness_period=10000)
208
+ except ValueError as e:
209
+ app.put_data(name, content=f"error: {e}".encode(), freshness_period=10000)
210
+
211
+ asyncio.create_task(handler())
212
+
213
+
214
+ if __name__ == '__main__':
215
+ print("Starting remote function node")
216
+ print(f" /remote_modify : /remote_modify/(<arg>)")
217
+ print(f" /temperature_average : /temperature_average/(<name1>, <name2>, ...)")
218
+ print(f" /format_temp : /format_temp/(<temp_value_or_func>)")
219
+ print(f" /m_to_feet : /m_to_feet/(<meters_value>)")
220
+ print(f" /data/* : temperature data")
221
+ print(f" Available data: {list(_TEMPERATURE_DATA.keys())}")
222
+ app.run_forever()
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from ndn.app import NDNApp
6
+ from ndn.encoding import Name
7
+ from ndn.security import KeychainDigest
8
+
9
+
10
+ class Server:
11
+ def __init__(self):
12
+ try:
13
+ self.app = NDNApp(keychain=KeychainDigest())
14
+ except Exception as e:
15
+ print(f"Error: Failed to initialize NDNApp: {e}", file=sys.stderr)
16
+ sys.exit(1)
17
+
18
+ def run(self):
19
+ @self.app.route('/data/ryu')
20
+ def on_data_ryu(name, param, _app_param):
21
+ print(f"Received Interest: {Name.to_str(name)}")
22
+ self.app.put_data(name, content=b'success', freshness_period=10000)
23
+ print(f"Sent Data: {Name.to_str(name)} -> success")
24
+ print("Server started. Listening for Interests on /data/ryu...")
25
+
26
+ @self.app.route('/data/nakazatolab')
27
+ def on_data_nakazatolab(name, param, _app_param):
28
+ print(f"Received Interest: {Name.to_str(name)}")
29
+ self.app.put_data(name, content=b'NDN research', freshness_period=10000)
30
+ print(f"Sent Data: {Name.to_str(name)} -> NDN research")
31
+ print("Server started. Listening for Interests on /data/nakazatolab...")
32
+
33
+ @self.app.route('/height/Mt.Fuji')
34
+ def on_height_mt_fuji(name, param, _app_param):
35
+ print(f"Received Interest: {Name.to_str(name)}")
36
+ self.app.put_data(name, content=b'3776m', freshness_period=10000)
37
+ print(f"Sent Data: {Name.to_str(name)} -> 3776m")
38
+ print("Server started. Listening for Interests on /height/Mt.Fuji...")
39
+
40
+ self.app.run_forever()
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: ndnc
3
+ Version: 0.0.1
4
+ Summary: A DSL interpreter for transparent distributed execution over NDN
5
+ Author-email: tryuuu <ryu23210@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/tryuuu/ndn-compiler
8
+ Project-URL: Repository, https://github.com/tryuuu/ndn-compiler
9
+ Keywords: ndn,dsl,distributed,icn
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Interpreters
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: lark==1.1.9
19
+ Requires-Dist: python-ndn
20
+ Dynamic: license-file
21
+
22
+ # Description
23
+ A minimal domain-specific language (DSL) interpreter for NDN-less syntax.
24
+ Currently supports several simple operations.
25
+ # Setup
26
+ ## Start Environment (Docker)
27
+ Build and start NFD and Producer containers.
28
+ ```bash
29
+ make all
30
+ ```
31
+ ## Run examples
32
+ Run the consumer in a container.
33
+ ```bash
34
+ make run
35
+ ```
36
+
37
+ ローカル関数(`modify`)の動作確認:
38
+ ```bash
39
+ make run S=examples/hello.ndn
40
+ # 出力例: local data from function
41
+ ```
42
+
43
+ リモート関数(`remote_modify`)の動作確認:
44
+ ```bash
45
+ make run S=examples/remote.ndn
46
+ # 出力例: local data from remote_modify
47
+ ```
48
+
49
+ `remote_modify` は NDN Interest `/remote_modify/<arg>` を発行し、`remote_modify` コンテナが処理を行う。`make all` 実行時に自動で起動する。
50
+ ## Check Logs
51
+ ```bash
52
+ make logs
53
+ ```
54
+ ## Stop Environment
55
+ ```bash
56
+ make down
57
+ ```
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/ndnc/__init__.py
5
+ src/ndnc/cli.py
6
+ src/ndnc/remote_modify.py
7
+ src/ndnc/server.py
8
+ src/ndnc.egg-info/PKG-INFO
9
+ src/ndnc.egg-info/SOURCES.txt
10
+ src/ndnc.egg-info/dependency_links.txt
11
+ src/ndnc.egg-info/entry_points.txt
12
+ src/ndnc.egg-info/requires.txt
13
+ src/ndnc.egg-info/top_level.txt
14
+ src/ndnc/interp/__init__.py
15
+ src/ndnc/interp/evaluator.py
16
+ src/ndnc/parser/__init__.py
17
+ src/ndnc/parser/ast.py
18
+ src/ndnc/parser/grammar.lark
19
+ src/ndnc/parser/parser.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ndnc = ndnc.cli:main
@@ -0,0 +1,2 @@
1
+ lark==1.1.9
2
+ python-ndn
@@ -0,0 +1 @@
1
+ ndnc