volaro 0.1.0-alpha.13 → 0.1.0-alpha.15

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Volaro
2
2
 
3
- **Limited alpha** (this is `0.1.0-alpha.13`), published under both the
3
+ **Limited alpha** (this is `0.1.0-alpha.15`), published under both the
4
4
  `latest` and `alpha` dist-tags: `npm install volaro`.
5
5
 
6
6
  Volaro is an experimental application language intended for AI authoring and
@@ -1,6 +1,6 @@
1
1
  {
2
- "revision": "d6e19b4e656bee01bf90048117fb24b598abdf3b",
2
+ "revision": "336e0ce359237ddff36fa674c51b098bacb2a295",
3
3
  "source": "git",
4
4
  "dirty": false,
5
- "content_sha256": "64c762006f1fceda3601d6c6b3a6de4ba99590d93aa14d04a749f318a840aa17"
5
+ "content_sha256": "588e82948fcb11b97bf14918badf69c335817fd8753f085c368bc6f1ded2f245"
6
6
  }
@@ -1 +1 @@
1
- d6e19b4e656bee01bf90048117fb24b598abdf3b
1
+ 336e0ce359237ddff36fa674c51b098bacb2a295
@@ -24,10 +24,12 @@ from .checks import check
24
24
  from .diagnostics import LineIndex, Severity
25
25
  from .lexer import Lexer
26
26
  from .parser import Parser
27
- from .resolve import resolve, resolve_page_server
27
+ from .resolve import resolve, resolve_page_server, resolve_server_ctx
28
28
  from .project_config import load_config, ConfigError
29
29
  from .diagnostics import Diagnostic
30
30
  from .layoutcompose import check_all as check_project_composition
31
+ from .pagemanifest import build_manifest as build_page_manifest
32
+ from .pageroutes import build_manifest as build_route_manifest
31
33
 
32
34
 
33
35
  def _iter_paths(args: list[str]) -> list[str]:
@@ -69,19 +71,32 @@ def check_source(src: str, filename: str, strict_names: bool = False,
69
71
  resolver = resolve_page_server
70
72
  else:
71
73
  mod = parser.parse_server_ctx()
72
- # A `_server.vl` holds only `context` providers; the resolver
73
- # has no pass for them yet.
74
- do_resolve = False
74
+ # A `_server.vl` holds only `context` providers. Their
75
+ # expressions resolve against what the build gives them: the
76
+ # project's app.vl, the ambient names, ancestor providers and
77
+ # earlier providers in the file (resolve.resolve_server_ctx).
78
+ resolver = resolve_server_ctx
75
79
  else:
76
80
  mod = parser.parse_module()
77
81
  diags = (list(lx.diags) + list(parser.diags)
78
82
  + check(mod, filename, li, strict_names=strict_names, release=release))
79
83
  if do_resolve:
80
84
  diags += resolver(mod, filename, li)
85
+ # An outbound call in a closure of server code is the server rule's
86
+ # E-API-SERVER-CLOSURE (the code the build gives it); the general
87
+ # E-ASYNC-CALL at the same call would say it twice.
88
+ server = {(d.start, d.end) for d in diags if d.code == "E-API-SERVER-CLOSURE"}
89
+ diags = [d for d in diags
90
+ if not (d.code == "E-ASYNC-CALL" and (d.start, d.end) in server)]
81
91
  diags.sort(key=lambda d: (d.start, 0 if d.severity == Severity.ERROR else 1, d.code))
82
92
  return li, diags
83
93
 
84
94
 
95
+ def _diag_key(d: Diagnostic) -> tuple:
96
+ return (os.path.normcase(os.path.abspath(d.file or "")), d.start, d.code,
97
+ d.message)
98
+
99
+
85
100
  def main(argv: list[str]) -> int:
86
101
  # Test-only hooks, same posture as vlbuild's VOLARO_TEST_BREACH_API
87
102
  # (server_emit.py): inert unless a test harness sets the env var,
@@ -128,9 +143,12 @@ def main(argv: list[str]) -> int:
128
143
  total_err = total_warn = 0
129
144
  json_rows: list[dict] = []
130
145
 
146
+ reported: set[tuple] = set()
147
+
131
148
  def report(li: LineIndex, diags: list[Diagnostic]) -> None:
132
149
  nonlocal total_err, total_warn
133
150
  for d in diags:
151
+ reported.add(_diag_key(d))
134
152
  if d.severity == Severity.ERROR:
135
153
  total_err += 1
136
154
  elif d.severity == Severity.WARNING:
@@ -161,11 +179,18 @@ def main(argv: list[str]) -> int:
161
179
  # A project directory also gets the cross-file page checks (spec
162
180
  # §8.15: layout-chain landmarks, nav names, static ids, literal link
163
181
  # targets), the same ones the project build enforces.
182
+ # Route discovery comes first, as in the project build, which refuses
183
+ # to start on any of it: browser pages (E-PAGE-*,
184
+ # E-PROJECT-ENTRY-AMBIGUOUS) and page-server routes (R-PS-*, E-PS-*).
185
+ # Parse errors it repeats were reported with their file above.
164
186
  if do_resolve:
165
187
  for root in (a for a in args if os.path.isdir(os.path.join(a, "app"))):
166
188
  by_file: dict[str, list[Diagnostic]] = {}
167
- for d in check_project_composition(root):
168
- by_file.setdefault(d.file, []).append(d)
189
+ for d in (build_page_manifest(root).diagnostics
190
+ + build_route_manifest(root).diagnostics
191
+ + check_project_composition(root)):
192
+ if _diag_key(d) not in reported:
193
+ by_file.setdefault(d.file, []).append(d)
169
194
  for path, diags in sorted(by_file.items()):
170
195
  with open(path, "r", encoding="utf-8") as fh:
171
196
  report(LineIndex(fh.read()), diags)
@@ -214,10 +214,11 @@ class Guard(Node):
214
214
  @dataclass
215
215
  class PageServerFile(Node):
216
216
  """Root of a `page.server.vl` file (dispatched on the basename, not a
217
- keyword). Top level is handlers plus at most one `errors` block — no
218
- `use` / `type` / other module items."""
217
+ keyword). Top level is `use` lines, handlers and at most one `errors`
218
+ block — no `type` / other module items (spec section 10.2, Imports)."""
219
219
  handlers: list = field(default_factory=list) # list[PageServerHandler]
220
220
  errors: list = field(default_factory=list) # list[ErrorMapEntry]
221
+ uses: list = field(default_factory=list) # list[Use]
221
222
 
222
223
 
223
224
  @dataclass
@@ -246,9 +247,12 @@ class ErrorMapEntry(Node):
246
247
  # --- _server.vl context providers (spec §10.2) ------------------------------
247
248
  @dataclass
248
249
  class ServerContextFile(Node):
249
- """Root of a `_server.vl` file (dispatched on the basename): only
250
- `context` provider declarations."""
250
+ """Root of a `_server.vl` file (dispatched on the basename): `use`
251
+ lines, `context` provider declarations and, in a multi-page project's
252
+ root `app/_server.vl` only, the project's `auth` block (spec 10.2)."""
251
253
  providers: list = field(default_factory=list) # list[ContextProvider]
254
+ uses: list = field(default_factory=list) # list[Use]
255
+ auth: Any = None # AuthDecl | None
252
256
 
253
257
 
254
258
  @dataclass
@@ -456,3 +460,11 @@ class Range(Node):
456
460
  class Pattern(Node):
457
461
  binds: list = field(default_factory=list)
458
462
  head: str = "" # constructor / variant / "_" ; "" for literal & structural patterns
463
+ # What the pattern tests, for the build (spec section 5.6): "wild" (`_`),
464
+ # "name" (a bare name: a fieldless variant, or a binding), "ctor"
465
+ # (`head(a, b)`), "lit" (`lo`), "range" (`lo..hi` / `lo..=hi`),
466
+ # "record" (`T{...}`), "list" (`[...]`) or "other".
467
+ kind: str = ""
468
+ lo: Any = None # A.Literal of a "lit" / "range" pattern
469
+ hi: Any = None # A.Literal upper bound of a "range" pattern
470
+ inclusive: bool = False
@@ -0,0 +1,395 @@
1
+ """Calls that wait, in positions that cannot wait (alpha.17).
2
+
3
+ A call to a service route, an outbound `api` client, a page-server client
4
+ (`api.ns.m()`), `auth.*`, or a `let` closure holding `try` / `par` returns
5
+ before its work finishes. In a view handler, a route body, a `spawn` task
6
+ or a closure that itself waits, the build awaits it wherever it sits
7
+ (`upper(f(x))`, `"{f(x)}"`, `f(x) + 1`, a condition). Anywhere else -- a
8
+ `derive`, a render value, a `state` initialiser, a plain closure -- the
9
+ value would be a pending promise, so:
10
+
11
+ - E-ASYNC-CALL: a network call in a position that cannot wait.
12
+ - E-CLOSURE-ASYNC: a call to a `let` closure holding `try` / `par` in a
13
+ position that cannot wait, or such a closure handed to a view prop the
14
+ child calls from one (the std-function case is in
15
+ `checks.Checker._check_async_closure_arg`).
16
+
17
+ Names are followed by binding, not by spelling (`Scopes`): a closure
18
+ parameter or a local that shadows a `try` / `par` closure is not that
19
+ closure. A callback prop is followed through the views that pass it on
20
+ (`Child(onSave: onSave)`), so a grandchild calling it where nothing can
21
+ wait counts for the view the closure was handed to.
22
+
23
+ The build (vlbuild.emit) makes the same decision at emission time and
24
+ refuses the same source with the same codes; `fn_params_called_sync` is
25
+ shared by both.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ import re
32
+
33
+ from . import ast_nodes as A
34
+
35
+ # Fix lines, shared with the build's messages (vlbuild.emit).
36
+ ASYNC_CALL_FIX = ("fetch it with `load NAME = CALL`, or call it in an `on` "
37
+ "handler or route body and keep the result in `state` or "
38
+ "a `let`")
39
+ CLOSURE_CALL_FIX = ("call it where the call can be awaited -- an `on` handler, "
40
+ "a route body, or another closure that holds `try` / "
41
+ "`par` -- and use the result there (keep it in `state` "
42
+ "for render code)")
43
+ PROP_ASYNC_FIX = ("call '%s' only from an `on` handler in %s, where it is "
44
+ "awaited, or pass a closure without `try` / `par`")
45
+ STD_ASYNC_FIX = ("use `CALL() ?? fallback` inside the closure for a call that "
46
+ "does not wait; make network calls in the handler or route "
47
+ "body first and bind the results with `let`")
48
+
49
+
50
+ def sentence(fix: str) -> str:
51
+ """A fix line as a sentence of a build message."""
52
+ return fix[:1].upper() + fix[1:]
53
+
54
+
55
+ def _children(node):
56
+ for fname in getattr(node, "__dataclass_fields__", {}):
57
+ if fname in ("start", "end"):
58
+ continue
59
+ val = getattr(node, fname)
60
+ for item in (val if isinstance(val, (list, tuple)) else [val]):
61
+ for sub in (item if isinstance(item, tuple) else (item,)):
62
+ if hasattr(sub, "__dataclass_fields__"):
63
+ yield sub
64
+
65
+
66
+ def _closure_async(e) -> bool:
67
+ from .checks import closure_needs_async
68
+ return closure_needs_async(e)
69
+
70
+
71
+ # Binding tags: what a name is bound to, as `Scopes.tag` finds it.
72
+ ASYNC = "async" # a `let` bound to a closure holding `try` / `par`
73
+
74
+
75
+ class Scopes:
76
+ """Lexical scopes, innermost last: each maps a name to its binding's
77
+ tag (`ASYNC`, `("param", name)` for a view's `fn` param, or None)."""
78
+
79
+ def __init__(self, names=()) -> None:
80
+ self.frames: list[dict] = [{n: None for n in names if n}]
81
+
82
+ def push(self, names=()) -> None:
83
+ self.frames.append({n: None for n in names if n})
84
+
85
+ def pop(self) -> None:
86
+ self.frames.pop()
87
+
88
+ def bind(self, name, tag=None) -> None:
89
+ if name:
90
+ self.frames[-1][name] = tag
91
+
92
+ def tag(self, name):
93
+ for frame in reversed(self.frames):
94
+ if name in frame:
95
+ return frame[name]
96
+ return None
97
+
98
+
99
+ def let_tag(st):
100
+ """`ASYNC` for `let f = fn ...` holding `try` / `par`, else None."""
101
+ return ASYNC if (isinstance(st.init, A.Closure) and _closure_async(st.init)) else None
102
+
103
+
104
+ def _block(stmts, is_async, sc, visit, names=()):
105
+ sc.push(names)
106
+ try:
107
+ for st in (stmts if isinstance(stmts, list) else [stmts]):
108
+ if st is not None:
109
+ walk(st, is_async, sc, visit)
110
+ finally:
111
+ sc.pop()
112
+
113
+
114
+ def walk(node, is_async, sc: Scopes, visit):
115
+ """Call `visit(node, is_async, sc)` for every node under `node`, with
116
+ `is_async` true where the build can `await` (an `on` handler, a
117
+ `spawn` task, a closure holding `try` / `par`; a plain closure body is
118
+ not) and `sc` holding the bindings in scope there: a block's `let`s, a
119
+ closure's params, a `for`'s variables, a `match` / `catch` arm's
120
+ bindings. A `let` is bound after its initialiser, which still sees the
121
+ outer binding; a `par` block's lets bind in the enclosing block."""
122
+ visit(node, is_async, sc)
123
+ if isinstance(node, A.Let):
124
+ if node.init is not None:
125
+ walk(node.init, is_async, sc, visit)
126
+ sc.bind(node.name, let_tag(node))
127
+ elif isinstance(node, (A.StateDecl, A.DeriveDecl, A.LoadDecl)):
128
+ for sub in _children(node):
129
+ walk(sub, is_async, sc, visit)
130
+ sc.bind(node.name)
131
+ elif isinstance(node, (A.OnBlock, A.Spawn)):
132
+ _block(node.body, True, sc, visit)
133
+ elif isinstance(node, A.Closure):
134
+ inner = _closure_async(node) if isinstance(node.body, list) else False
135
+ _block(node.body, inner, sc, visit, [p.name for p in node.params])
136
+ elif isinstance(node, A.For):
137
+ walk(node.iter, is_async, sc, visit)
138
+ _block(node.body, is_async, sc, visit, node.vars)
139
+ elif isinstance(node, A.While):
140
+ walk(node.cond, is_async, sc, visit)
141
+ _block(node.body, is_async, sc, visit)
142
+ elif isinstance(node, A.If):
143
+ walk(node.cond, is_async, sc, visit)
144
+ _block(node.then, is_async, sc, visit)
145
+ for cond, blk in node.elifs or []:
146
+ walk(cond, is_async, sc, visit)
147
+ _block(blk, is_async, sc, visit)
148
+ if node.orelse:
149
+ _block(node.orelse, is_async, sc, visit)
150
+ elif isinstance(node, A.Match):
151
+ walk(node.subject, is_async, sc, visit)
152
+ for arm in node.arms:
153
+ sc.push(getattr(arm[0], "binds", None) or ())
154
+ try:
155
+ for part in arm[1:]:
156
+ for st in (part if isinstance(part, list) else [part]):
157
+ if st is not None:
158
+ walk(st, is_async, sc, visit)
159
+ finally:
160
+ sc.pop()
161
+ elif isinstance(node, A.Try):
162
+ walk(node.expr, is_async, sc, visit)
163
+ for pat, body in node.catches:
164
+ _block(body, is_async, sc, visit, getattr(pat, "binds", None) or ())
165
+ elif isinstance(node, A.Element):
166
+ for sub in node.args or []:
167
+ walk(sub, is_async, sc, visit)
168
+ for _k, v in node.attrs or []:
169
+ if hasattr(v, "__dataclass_fields__"):
170
+ walk(v, is_async, sc, visit)
171
+ _block(node.body, is_async, sc, visit)
172
+ else:
173
+ for sub in _children(node):
174
+ walk(sub, is_async, sc, visit)
175
+
176
+
177
+ def load_calls(expr) -> list:
178
+ """The calls a `load` awaits itself: its call, and each call on the
179
+ left of a `??` chain after it (`load v = f() ?? g() ?? d`)."""
180
+ out = []
181
+ while True:
182
+ if isinstance(expr, A.Call):
183
+ out.append(expr)
184
+ return out
185
+ if (isinstance(expr, A.Binary) and expr.op == "??"
186
+ and isinstance(expr.left, A.Call)):
187
+ out.append(expr.left)
188
+ expr = expr.right
189
+ continue
190
+ return out
191
+
192
+
193
+ def literal_exprs(lit) -> list:
194
+ """Each `{expr}` of a string literal, parsed from the literal's own
195
+ text as the build parses it (offsets are not the file's); one that
196
+ does not parse is left out (the build reports it)."""
197
+ from .lexer import Lexer
198
+ from .parser import Parser
199
+ out = []
200
+ raw = getattr(lit, "raw", "") or ""
201
+ for s, e in getattr(lit, "interp_spans", ()) or ():
202
+ text = re.sub(r'\\(["\\])', r"\1", raw[s - lit.start:e - lit.start])
203
+ try:
204
+ out.append(Parser(Lexer(text, "<interp>").lex(), text,
205
+ "<interp>").parse_expr())
206
+ except Exception: # noqa: BLE001
207
+ continue
208
+ return out
209
+
210
+
211
+ def fn_params_called_sync(view, scope_of=None, _memo=None) -> set[str]:
212
+ """The `fn`-typed params of `view` that end up called from a position
213
+ that cannot wait (a derive, render, a `state` initialiser, a plain
214
+ closure): by `view` itself, or by a view it passes the param on to
215
+ (`Child(onSave: onSave)`), however deep. `scope_of(view)` gives the
216
+ views that view's body can render, by name (its file's own and its
217
+ imports); without it only `view`'s own calls count. A parent must not
218
+ hand a closure that waits to one of these."""
219
+ memo = {} if _memo is None else _memo
220
+ if id(view) in memo:
221
+ return memo[id(view)] # done, or in progress (a cycle): what is known
222
+ found: set[str] = set()
223
+ memo[id(view)] = found
224
+ fn_params = [p.name for p in view.params
225
+ if isinstance(p.type, A.TypeRef) and p.type.is_fn]
226
+ if not fn_params:
227
+ return found
228
+ scope = (scope_of(view) if scope_of else None) or {}
229
+
230
+ def visit(node, is_async, sc):
231
+ if isinstance(node, A.Literal):
232
+ for sub in literal_exprs(node):
233
+ walk(sub, is_async, sc, visit)
234
+ elif isinstance(node, A.Call) and isinstance(node.callee, A.Name):
235
+ t = sc.tag(node.callee.id)
236
+ if not is_async and isinstance(t, tuple):
237
+ found.add(t[1])
238
+ elif isinstance(node, A.Element) and node.name in scope:
239
+ passed = {k: sc.tag(v.id) for k, v in node.attrs
240
+ if isinstance(v, A.Name) and isinstance(sc.tag(v.id), tuple)}
241
+ if passed:
242
+ sync = fn_params_called_sync(scope[node.name], scope_of, memo)
243
+ found.update(t[1] for k, t in passed.items() if k in sync)
244
+
245
+ sc = Scopes([p.name for p in view.params])
246
+ for p in fn_params:
247
+ sc.bind(p, ("param", p))
248
+ for st in view.body:
249
+ walk(st, False, sc, visit)
250
+ return found
251
+
252
+
253
+ def _parse(path: str):
254
+ from .lexer import Lexer
255
+ from .parser import Parser
256
+ with open(path, encoding="utf-8") as fh:
257
+ src = fh.read()
258
+ return Parser(Lexer(src, path).lex(), src, path).parse_module()
259
+
260
+
261
+ def _use_target(base: str, it) -> str | None:
262
+ if not (isinstance(it, A.Use) and it.path.startswith(".")):
263
+ return None
264
+ cand = os.path.normpath(os.path.join(base, it.path))
265
+ target = next((c for c in (cand + ".vl", os.path.join(cand, "index.vl"), cand)
266
+ if os.path.isfile(c)), None)
267
+ return os.path.normcase(os.path.abspath(target)) if target else None
268
+
269
+
270
+ def imported_decls(filename: str, mod):
271
+ """(`api` block names, views, scope_of) the way the build loads them:
272
+ the `api` blocks this file imports with a relative `use`, the views
273
+ it can render (its own and imported), and `scope_of(view)` -> the
274
+ views that view's own file can render, following every imported
275
+ file's own `use`s."""
276
+ files: dict[str, tuple] = {} # path -> (module, the views it can render)
277
+ scopes: dict[int, dict] = {} # id(view) -> its file's views
278
+
279
+ def file_scope(path: str, module) -> dict:
280
+ if path in files:
281
+ return files[path][1]
282
+ scope: dict = {}
283
+ files[path] = (module, scope)
284
+ base = os.path.dirname(path)
285
+ for it in getattr(module, "items", []):
286
+ if isinstance(it, A.ViewDecl):
287
+ scope[it.name] = it
288
+ scopes[id(it)] = scope
289
+ for it in getattr(module, "items", []):
290
+ target = _use_target(base, it)
291
+ if target is None:
292
+ continue
293
+ if target not in files:
294
+ try:
295
+ file_scope(target, _parse(target))
296
+ except Exception: # noqa: BLE001 -- its own check reports it
297
+ continue
298
+ wanted = set(it.names or [])
299
+ for d in files[target][0].items:
300
+ if isinstance(d, A.ViewDecl) and (not wanted or d.name in wanted):
301
+ scope.setdefault(d.name, d)
302
+ return scope
303
+
304
+ path = os.path.normcase(os.path.abspath(filename))
305
+ views = dict(file_scope(path, mod))
306
+ apis: set[str] = set()
307
+ for it in getattr(mod, "items", []):
308
+ target = _use_target(os.path.dirname(path), it)
309
+ if target is None or target not in files:
310
+ continue
311
+ wanted = set(it.names or [])
312
+ apis.update(d.name for d in files[target][0].items
313
+ if isinstance(d, A.ApiDecl) and (not wanted or d.name in wanted))
314
+ return apis, views, (lambda v: scopes.get(id(v), {}))
315
+
316
+
317
+ class AsyncCalls:
318
+ def __init__(self, report, clients: set, views: dict, interp=None,
319
+ scope_of=None):
320
+ self.report = report # report(code, message, node, fix)
321
+ self.interp = interp # (start, end) -> parsed `{expr}` or None
322
+ self.clients = clients # service and api block names
323
+ self.views = views # the views this file can render, by name
324
+ self.scope_of = scope_of # view -> the views its file can render
325
+ self._memo: dict = {}
326
+
327
+ def kind(self, call, sc: Scopes):
328
+ """"net" for a network call, "closure" for a call to a `let`
329
+ closure holding `try` / `par`, else None."""
330
+ callee = call.callee
331
+ if isinstance(callee, A.Member):
332
+ obj = callee.obj
333
+ # `auth.sign_in(...)`, `calc.one()`, `store.get(...)`, and the
334
+ # page-server client `api.posts.get(...)` / legacy `api.x()`
335
+ if isinstance(obj, A.Name) and (
336
+ obj.id in ("api", "auth") or obj.id in self.clients):
337
+ return "net"
338
+ if (isinstance(obj, A.Member) and isinstance(obj.obj, A.Name)
339
+ and obj.obj.id == "api"):
340
+ return "net"
341
+ return None
342
+ if isinstance(callee, A.Name) and sc.tag(callee.id) == ASYNC:
343
+ return "closure"
344
+ return None
345
+
346
+ def run(self, stmts, is_async: bool, params=()) -> None:
347
+ """One declaration's body (a view, a route, a page.server handler),
348
+ with its params in scope."""
349
+ allowed: set[int] = set()
350
+
351
+ def visit(node, ctx, sc):
352
+ if isinstance(node, A.Literal) and self.interp is not None:
353
+ # `"{f(x)}"`: the build lowers each `{expr}` in place
354
+ for s, e in getattr(node, "interp_spans", ()) or ():
355
+ sub = self.interp(s, e)
356
+ if sub is not None:
357
+ walk(sub, ctx, sc, visit)
358
+ elif isinstance(node, A.LoadDecl):
359
+ allowed.update(id(c) for c in load_calls(node.expr))
360
+ elif isinstance(node, A.Element) and node.name in self.views:
361
+ sync = fn_params_called_sync(self.views[node.name], self.scope_of,
362
+ self._memo)
363
+ for k, v in node.attrs:
364
+ waits = ((isinstance(v, A.Closure) and _closure_async(v))
365
+ or (isinstance(v, A.Name) and sc.tag(v.id) == ASYNC))
366
+ if waits and k in sync:
367
+ self.report(
368
+ "E-CLOSURE-ASYNC",
369
+ "the closure passed as '%s' holds `try` or `par`, "
370
+ "but %s calls '%s', itself or through a view it "
371
+ "passes it to, from a derive, render value or plain "
372
+ "closure, which cannot wait for it" % (
373
+ k, node.name, k), v,
374
+ PROP_ASYNC_FIX % (k, node.name))
375
+ elif (isinstance(node, A.Call) and not ctx
376
+ and id(node) not in allowed):
377
+ kind = self.kind(node, sc)
378
+ if kind == "net":
379
+ self.report(
380
+ "E-ASYNC-CALL",
381
+ "this call waits for a network call, and a derive, "
382
+ "render value, `state` initialiser or plain closure "
383
+ "cannot wait for it: it would get a pending value",
384
+ node, ASYNC_CALL_FIX)
385
+ elif kind == "closure":
386
+ self.report(
387
+ "E-CLOSURE-ASYNC",
388
+ "'%s' holds `try` or `par`, so a call to it waits, and "
389
+ "a derive, render value, `state` initialiser or plain "
390
+ "closure cannot wait for it: it would get a pending "
391
+ "value" % node.callee.id, node, CLOSURE_CALL_FIX)
392
+
393
+ sc = Scopes(params)
394
+ for st in stmts or []:
395
+ walk(st, is_async, sc, visit)