volaro 0.1.0-alpha.11 → 0.1.0-alpha.13
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 +1 -1
- package/compiler/SOURCE_INFO.json +2 -2
- package/compiler/SOURCE_REV +1 -1
- package/compiler/validator/vlcheck/__main__.py +10 -6
- package/compiler/validator/vlcheck/checks.py +291 -57
- package/compiler/validator/vlcheck/resolve.py +144 -17
- package/compiler/vlbuild/vlbuild/assets/vlrt.js +68 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.min.js +1 -1
- package/compiler/vlbuild/vlbuild/emit.py +193 -55
- package/compiler/vlbuild/vlbuild/project.py +4 -2
- package/compiler/vlbuild/vlbuild/server_emit.py +38 -72
- package/language/crib.md +9 -1
- package/language/spec.md +1 -1
- package/language/supported.md +72 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Volaro
|
|
2
2
|
|
|
3
|
-
**Limited alpha** (this is `0.1.0-alpha.
|
|
3
|
+
**Limited alpha** (this is `0.1.0-alpha.13`), 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": "
|
|
2
|
+
"revision": "d6e19b4e656bee01bf90048117fb24b598abdf3b",
|
|
3
3
|
"source": "git",
|
|
4
4
|
"dirty": false,
|
|
5
|
-
"content_sha256": "
|
|
5
|
+
"content_sha256": "64c762006f1fceda3601d6c6b3a6de4ba99590d93aa14d04a749f318a840aa17"
|
|
6
6
|
}
|
package/compiler/SOURCE_REV
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
d6e19b4e656bee01bf90048117fb24b598abdf3b
|
|
@@ -24,7 +24,7 @@ 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
|
|
27
|
+
from .resolve import resolve, resolve_page_server
|
|
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
|
|
@@ -59,21 +59,25 @@ def check_source(src: str, filename: str, strict_names: bool = False,
|
|
|
59
59
|
# (spec §10.2): handlers and an `errors` block, no module items. Every
|
|
60
60
|
# other basename parses as an ordinary module.
|
|
61
61
|
base = os.path.basename(filename)
|
|
62
|
+
resolver = resolve
|
|
62
63
|
if base in ("page.server.vl", "_server.vl"):
|
|
63
64
|
if base == "page.server.vl":
|
|
64
65
|
mod = parser.parse_page_server()
|
|
66
|
+
# Handler bodies resolve like service route bodies, against the
|
|
67
|
+
# project's app.vl, the handler's params and the `_server.vl`
|
|
68
|
+
# context providers above the file (resolve.resolve_page_server).
|
|
69
|
+
resolver = resolve_page_server
|
|
65
70
|
else:
|
|
66
71
|
mod = parser.parse_server_ctx()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
do_resolve = False
|
|
72
|
+
# A `_server.vl` holds only `context` providers; the resolver
|
|
73
|
+
# has no pass for them yet.
|
|
74
|
+
do_resolve = False
|
|
71
75
|
else:
|
|
72
76
|
mod = parser.parse_module()
|
|
73
77
|
diags = (list(lx.diags) + list(parser.diags)
|
|
74
78
|
+ check(mod, filename, li, strict_names=strict_names, release=release))
|
|
75
79
|
if do_resolve:
|
|
76
|
-
diags +=
|
|
80
|
+
diags += resolver(mod, filename, li)
|
|
77
81
|
diags.sort(key=lambda d: (d.start, 0 if d.severity == Severity.ERROR else 1, d.code))
|
|
78
82
|
return li, diags
|
|
79
83
|
|
|
@@ -105,12 +105,13 @@ _METHOD_FN_FORM = {
|
|
|
105
105
|
"matches": "matches", "match": "matches", "test": "matches",
|
|
106
106
|
"first_char": "first_char", "split": "split", "join": "join",
|
|
107
107
|
"filter": "filter", "map": "map", "take": "take", "count": "count",
|
|
108
|
+
"some": "any", "every": "all",
|
|
108
109
|
"sum": "sum", "sort_by": "sort_by", "group_by": "group_by",
|
|
109
110
|
"chunk": "chunk", "concat": "concat", "append": "append", "push": "append",
|
|
110
111
|
"keys": "keys", "values": "values", "get_or": "get_or",
|
|
111
112
|
}
|
|
112
113
|
_LIST_FNS = {"filter", "map", "take", "count", "sum", "sort_by", "group_by",
|
|
113
|
-
"chunk", "concat", "append", "join"}
|
|
114
|
+
"chunk", "concat", "append", "join", "any", "all"}
|
|
114
115
|
# Other common carry-overs with a Volaro spelling that is not a function call.
|
|
115
116
|
_METHOD_OTHER_FIX = {
|
|
116
117
|
"length": "`{r}.len()`", "size": "`{r}.len()`",
|
|
@@ -124,64 +125,200 @@ _METHOD_OTHER_FIX = {
|
|
|
124
125
|
# Names a page-server / service body has in scope that are ambient objects,
|
|
125
126
|
# not user values.
|
|
126
127
|
_AMBIENT_RECEIVERS = {"session", "request"}
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
128
|
+
# The std free-function surface, in one place. Every std function name the
|
|
129
|
+
# checker knows is in exactly one of the two tables below:
|
|
130
|
+
#
|
|
131
|
+
# STD_BUILT_FNS -- vlbuild lowers `name(...)` to `VL.std.name(...)`.
|
|
132
|
+
# vlbuild's `emit.STD_FNS` IS this set (imported, not
|
|
133
|
+
# copied), and `VL.std` in `assets/vlrt.js` defines each
|
|
134
|
+
# one; server.js embeds that same `VL.std` block, so a
|
|
135
|
+
# route body gets the identical function.
|
|
136
|
+
# _FN_NOT_BUILT -- a name the spec, the corpus or an agent uses that this
|
|
137
|
+
# build has no function for. A bare call where the build
|
|
138
|
+
# emits it (a view or a service route) is E-FN-NOT-BUILT
|
|
139
|
+
# with this fix line; a module-level `fn` body is never
|
|
140
|
+
# emitted into either, so it is not judged there.
|
|
141
|
+
#
|
|
142
|
+
# resolve.py's known-name list is derived from these two, so a name cannot be
|
|
143
|
+
# "known" without being classified. `test_std_fn_parity` in vlbuild's
|
|
144
|
+
# tests/test_build.py calls every known name through the real checker and
|
|
145
|
+
# requires each accepted one to exist in the emitter, `vlrt.js`, its minified
|
|
146
|
+
# copy and server.js. This replaced five hand-synced tables: `unique` was
|
|
147
|
+
# known to the resolver, missing from the other four, and built to a bare
|
|
148
|
+
# `unique(...)` that crashed every page (Testing Lab search brief, Example
|
|
149
|
+
# Demo 2.3).
|
|
150
|
+
STD_BUILT_FNS = frozenset({
|
|
151
|
+
# std.list
|
|
152
|
+
"filter", "map", "take", "count", "sum", "sort_by", "group_by", "chunk",
|
|
153
|
+
# list growth (spec section 20.9): the only way to combine or grow a list
|
|
154
|
+
"concat", "append",
|
|
155
|
+
# `unique(xs)`: first occurrence of each distinct value, in order
|
|
156
|
+
"unique",
|
|
157
|
+
# `any(xs, f)` / `all(xs, f)`: whether some / every item passes `f`
|
|
158
|
+
"any", "all",
|
|
159
|
+
# std.str (spec section 13: split, join, trim, case, replace, contains,
|
|
160
|
+
# regex -- `matches`)
|
|
161
|
+
"join", "split", "first_char", "trim", "upper", "lower", "replace",
|
|
162
|
+
"contains", "matches",
|
|
163
|
+
# std.map
|
|
164
|
+
"keys", "values", "get_or",
|
|
165
|
+
# numeric conversion
|
|
166
|
+
"int_to_float", "float_to_int",
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
_FOLD_FIX = "a `for` loop over a `let mut` accumulator"
|
|
130
170
|
_FN_NOT_BUILT = {
|
|
131
|
-
|
|
132
|
-
"fold":
|
|
171
|
+
# in the spec (section 13 or its examples), not built
|
|
172
|
+
"fold": _FOLD_FIX,
|
|
133
173
|
"merge": "a record literal naming every field",
|
|
134
174
|
"to_int": "no string-to-int conversion is built yet; keep the value an int",
|
|
175
|
+
"now": ("a `model` field default (`created_at time = now()`); a view or "
|
|
176
|
+
"route cannot read the clock in this build"),
|
|
177
|
+
# names the checker has accepted, not built
|
|
178
|
+
"sorted": "sort_by(xs, fn(x) x)",
|
|
179
|
+
"first": "xs[0] (check xs.len() > 0 first)",
|
|
180
|
+
"reduce": _FOLD_FIX,
|
|
181
|
+
"flatten": _FOLD_FIX + " and `concat(acc, x)`",
|
|
182
|
+
"reverse": _FOLD_FIX + " and `concat([x], acc)`",
|
|
183
|
+
"zip": "a `for i in 0..xs.len()` loop reading `xs[i]` and `ys[i]`",
|
|
184
|
+
"enumerate": "a `for i in 0..xs.len()` loop reading `xs[i]`",
|
|
185
|
+
"index_of": "a `for i in 0..xs.len()` loop that compares `xs[i]`",
|
|
186
|
+
"slice": "take(xs, n) for a prefix, or a `for i in a..b` loop",
|
|
187
|
+
"range": "a `for i in 0..n` loop",
|
|
188
|
+
"none": "not any(xs, f)",
|
|
189
|
+
"avg": "sum(xs) / count(xs) (check count(xs) > 0 first)",
|
|
190
|
+
"min": "if (a < b) a else b",
|
|
191
|
+
"max": "if (a > b) a else b",
|
|
192
|
+
"abs": "if (x < 0) -x else x",
|
|
193
|
+
"round": "float_to_int(x + 0.5) for a non-negative x",
|
|
194
|
+
"floor": "float_to_int(x) for a non-negative x",
|
|
195
|
+
"ceil": "float_to_int(x) + 1 when x has a fraction, for a non-negative x",
|
|
196
|
+
"starts_with": "matches(s, \"^prefix\")",
|
|
197
|
+
"ends_with": "matches(s, \"suffix$\")",
|
|
198
|
+
"pad_start": "string interpolation: `\"{pad}{s}\"`",
|
|
199
|
+
"pad_end": "string interpolation: `\"{s}{pad}\"`",
|
|
200
|
+
"repeat": _FOLD_FIX + " building the string",
|
|
201
|
+
"uuid4": "let the database assign the id (`id uuid pk`)",
|
|
202
|
+
"random": "no random source is built yet",
|
|
135
203
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
# has no lowering for). Duplicated from `vlbuild/emit.py`'s `STD_FNS` (plus
|
|
142
|
-
# `now`, whose one working lowering — a `model` field default — lives in
|
|
143
|
-
# `server_emit.py`'s `_sql_default`) rather than imported: `vlcheck` has no
|
|
144
|
-
# dependency on `vlbuild` (the dependency runs the other way), so the two
|
|
145
|
-
# packages cannot share this table directly. Keep in sync by hand; a mismatch
|
|
146
|
-
# only makes this check too strict or too loose, never silently wrong, since
|
|
147
|
-
# `vlbuild` is the actual authority on what it lowers.
|
|
204
|
+
assert not (STD_BUILT_FNS & set(_FN_NOT_BUILT))
|
|
205
|
+
BUILTINS.update(STD_BUILT_FNS, _FN_NOT_BUILT)
|
|
206
|
+
# The names `use std.X { ... }` may import: every built function, plus `now`,
|
|
207
|
+
# whose one working lowering -- a `model` field default -- is server_emit.py's
|
|
208
|
+
# `_sql_default`.
|
|
148
209
|
#
|
|
149
210
|
# Found reproducing the login-page benchmark's Bug 3 (`test-only`
|
|
150
|
-
# `login-page/volaro/NOTES.md`): `use std.http { post }`
|
|
151
|
-
# clean and
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
211
|
+
# `login-page/volaro/NOTES.md`): `use std.http { post }` passed `vlcheck`
|
|
212
|
+
# clean and compiled to a bare, undefined `post(...)` call. `std.http` has no
|
|
213
|
+
# member here; nothing under it can pass this check.
|
|
214
|
+
_STD_SUPPORTED_NAMES = STD_BUILT_FNS | {"now"}
|
|
215
|
+
|
|
216
|
+
# Bare words the spec uses in argument position (`.order(created_at: desc)`,
|
|
217
|
+
# `auth.start_oauth(google)`). They resolve as names; resolve.py reads this
|
|
218
|
+
# set.
|
|
219
|
+
CONTEXTUAL_WORDS = frozenset({"asc", "desc", "google", "github"})
|
|
220
|
+
|
|
221
|
+
# Every other name the checker knows is not a function the build lowers, so
|
|
222
|
+
# a bare call to one compiles to an undefined (or non-function) JavaScript
|
|
223
|
+
# callee and throws when the page or route runs. Each is classified here by
|
|
224
|
+
# the one place it may be called, if any; `_check_builtin_call` rejects the
|
|
225
|
+
# call everywhere else in emitted code. `test_bare_call_parity` (vlbuild
|
|
226
|
+
# tests/test_build.py, gate 2) calls every known name in every emitted
|
|
227
|
+
# context and requires the checker to reject it or the build to lower it,
|
|
228
|
+
# and `_unclassified_names()` must stay empty, so a name added to BUILTINS
|
|
229
|
+
# cannot slip through unjudged. (Testing Lab, Example Demo 2.3: `unique`
|
|
230
|
+
# was the std case; these are the rest of the class.)
|
|
231
|
+
#
|
|
232
|
+
# `where()` predicates (spec section 13.1): a predicate is only a
|
|
233
|
+
# `.where(field: pred(x))` value (or a branch of an `if` that is one). The
|
|
234
|
+
# fix is the operator that tests the same thing on a value. `contains` is
|
|
235
|
+
# also a std.str function, so it is built everywhere.
|
|
236
|
+
_WHERE_PREDICATE_FIX = {
|
|
237
|
+
"lt": "`a < b`", "lte": "`a <= b`", "gt": "`a > b`", "gte": "`a >= b`",
|
|
238
|
+
"ne": "`a != b`", "between": "`lo <= x and x <= hi`",
|
|
239
|
+
"any_of": "`any(xs, fn(v) v == x)`",
|
|
240
|
+
}
|
|
241
|
+
# Error codes (spec section 6): a code is constructed only as the argument of
|
|
242
|
+
# `err(...)` in a service or page-server route body, where the build turns
|
|
243
|
+
# it into the route's error response. `catch not_found` and a match arm name
|
|
244
|
+
# a code without calling it.
|
|
245
|
+
_ERROR_CODES = frozenset({
|
|
246
|
+
"not_found", "forbidden", "conflict", "rejected", "unexpected",
|
|
247
|
+
"out_of_range", "decode", "invalid_credentials", "unverified_email",
|
|
248
|
+
"totp_required", "rate_limited",
|
|
249
|
+
})
|
|
250
|
+
# The codes the compiler-owned auth endpoints fail with (`catch
|
|
251
|
+
# weak_password`); vlbuild's emitter reads this set.
|
|
252
|
+
AUTH_ERROR_CODES = frozenset({
|
|
253
|
+
"email_taken", "weak_password", "breached_password", "rate_limited",
|
|
254
|
+
"invalid_credentials", "unverified_email", "totp_required",
|
|
255
|
+
"provider_error", "invalid_token", "expired_token", "invalid_code",
|
|
256
|
+
"no_totp_enrolled",
|
|
257
|
+
})
|
|
258
|
+
# A route body's `catch <head>` tests the thrown error's code when <head> is
|
|
259
|
+
# one of these (or a declared union variant); any other head binds the
|
|
260
|
+
# caught error itself (`catch e`). resolve.py and vlbuild's `_route_try`
|
|
261
|
+
# both read this set, so `err(e)` means the same thing to check and build.
|
|
262
|
+
ROUTE_CATCH_CODES = (_ERROR_CODES | AUTH_ERROR_CODES
|
|
263
|
+
| frozenset({"bad_request", "unauthorized"}))
|
|
264
|
+
_ERR_ROUTE_FIX = ("`return err(not_found(\"...\"))` in a `service` or "
|
|
265
|
+
"page.server route body; a view reports a failure by "
|
|
266
|
+
"setting its own state")
|
|
267
|
+
# Names that are never called: namespaces and ambient objects (call a member
|
|
268
|
+
# instead), type names (no conversion function by that name), contextual
|
|
269
|
+
# words, and names the spec's examples use that no build provides.
|
|
270
|
+
_NO_BUILD_NAMESPACE = "no `%s` namespace is built yet"
|
|
271
|
+
_NOT_CALLABLE = {
|
|
272
|
+
# namespaces / ambient objects
|
|
273
|
+
"env": "read a member: `env.NAME`",
|
|
274
|
+
"session": "read a member: `session.user`",
|
|
275
|
+
"request": "read a member: `request.headers`",
|
|
276
|
+
"route": "`route.to(\"/path\")`",
|
|
277
|
+
"auth": "a section 11.3 call: `auth.sign_in(email: e, password: p)`",
|
|
278
|
+
"db": "a query: `db.posts.where(...).all()?`",
|
|
279
|
+
"log": "`log.info(\"message\", value)`",
|
|
280
|
+
"api": "an `api` block endpoint: `client.endpoint(...)`",
|
|
281
|
+
"analytics": _NO_BUILD_NAMESPACE % "analytics",
|
|
282
|
+
"events": _NO_BUILD_NAMESPACE % "events",
|
|
283
|
+
"ws": _NO_BUILD_NAMESPACE % "ws",
|
|
284
|
+
"mailer": _NO_BUILD_NAMESPACE % "mailer",
|
|
285
|
+
"billing": _NO_BUILD_NAMESPACE % "billing",
|
|
286
|
+
# result constructor: a `T!` success is the plain value (spec section 6)
|
|
287
|
+
"ok": "return the value itself; a successful `T!` is the plain value",
|
|
288
|
+
# names from the spec's examples, not built-in functions
|
|
289
|
+
"create_guest": "no `create_guest` is built in; build the value inline",
|
|
290
|
+
"guest_user": "no `guest_user` is built in; build the value inline",
|
|
291
|
+
# contextual words
|
|
292
|
+
"asc": "an `.order(field: asc)` value, not a call",
|
|
293
|
+
"desc": "an `.order(field: desc)` value, not a call",
|
|
294
|
+
"google": "an `auth.start_oauth(google)` argument, not a call",
|
|
295
|
+
"github": "an `auth.start_oauth(github)` argument, not a call",
|
|
296
|
+
# type names
|
|
297
|
+
"str": "string interpolation: `\"{x}\"`",
|
|
298
|
+
"int": "float_to_int(x)",
|
|
299
|
+
"float": "int_to_float(x)",
|
|
300
|
+
"bool": "a comparison: `x != 0`, `s != \"\"`",
|
|
301
|
+
"bytes": "no conversion to `bytes` is built",
|
|
302
|
+
"time": "a `time` field value; no conversion to `time` is built",
|
|
303
|
+
"dur": "no conversion to `dur` is built",
|
|
304
|
+
"uuid": "let the database assign the id (`id uuid pk`)",
|
|
305
|
+
"json": "no conversion to `json` is built",
|
|
306
|
+
"nil": "the value `nil`, not a call",
|
|
182
307
|
}
|
|
183
308
|
|
|
184
309
|
|
|
310
|
+
def _unclassified_names() -> set[str]:
|
|
311
|
+
"""Known names with no bare-call rule: must stay empty (asserted below
|
|
312
|
+
and by test_bare_call_parity)."""
|
|
313
|
+
classified = (set(STD_BUILT_FNS) | set(_FN_NOT_BUILT) | set(_METHOD_ONLY)
|
|
314
|
+
| set(_WHERE_PREDICATE_FIX) | _ERROR_CODES | {"err"}
|
|
315
|
+
| set(_NOT_CALLABLE) | set(ELEMENT_NAMES))
|
|
316
|
+
return (set(BUILTINS) | set(_PRIM_NAMES) | CONTEXTUAL_WORDS) - classified
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
assert not _unclassified_names(), _unclassified_names()
|
|
320
|
+
|
|
321
|
+
|
|
185
322
|
def _lit_text(v):
|
|
186
323
|
"""The literal text of a plain string literal, or None when `v` is not one
|
|
187
324
|
(a bare word, an expression, or an *interpolated* string -- whose runtime
|
|
@@ -324,6 +461,17 @@ class Checker:
|
|
|
324
461
|
# (module-level fn bodies)
|
|
325
462
|
self._module_names: set[str] = set()
|
|
326
463
|
self._emitted = True
|
|
464
|
+
# which emitted code is being walked: "view" (a view's state,
|
|
465
|
+
# derives, render tree and handlers) or "route" (a service route,
|
|
466
|
+
# a page.server handler, a _server.vl provider). `err(...)` builds
|
|
467
|
+
# only in a route.
|
|
468
|
+
self._ctx = "route"
|
|
469
|
+
# payload-carrying union variants this module declares, and the
|
|
470
|
+
# calls whose position makes them legal (a `where()` predicate
|
|
471
|
+
# value, the error constructor inside `err(...)`)
|
|
472
|
+
self._variants: set[str] = set()
|
|
473
|
+
self._views: set[str] = set()
|
|
474
|
+
self._exempt_calls: set[int] = set()
|
|
327
475
|
# names of `fn`-typed record fields in this module: `x.on_pick(id)`
|
|
328
476
|
# calls one, which E-METHOD-UNKNOWN must not report
|
|
329
477
|
self._fn_fields: set[str] = set()
|
|
@@ -2973,10 +3121,15 @@ class Checker:
|
|
|
2973
3121
|
if nm:
|
|
2974
3122
|
top.add(nm)
|
|
2975
3123
|
self._module_names.add(nm)
|
|
3124
|
+
if isinstance(it, A.ViewDecl):
|
|
3125
|
+
self._views.add(nm)
|
|
2976
3126
|
if isinstance(it, A.Use):
|
|
2977
3127
|
for n in it.names:
|
|
2978
3128
|
top.add(n)
|
|
2979
|
-
|
|
3129
|
+
# `use std.time { now }` names the std function, not an
|
|
3130
|
+
# author's own: its call is still judged below
|
|
3131
|
+
if not it.path.startswith("std."):
|
|
3132
|
+
self._module_names.add(n)
|
|
2980
3133
|
base = it.path.replace("./", "").replace("../", "")
|
|
2981
3134
|
base = base.split("/")[-1].split(".")[-1]
|
|
2982
3135
|
if base:
|
|
@@ -2987,6 +3140,7 @@ class Checker:
|
|
|
2987
3140
|
elif isinstance(it, A.TypeDecl) and it.kind == "union":
|
|
2988
3141
|
for v in it.variants:
|
|
2989
3142
|
top.add(v.name)
|
|
3143
|
+
self._variants.add(v.name)
|
|
2990
3144
|
elif isinstance(it, (A.TypeDecl, A.ModelDecl)):
|
|
2991
3145
|
pass
|
|
2992
3146
|
|
|
@@ -3006,7 +3160,9 @@ class Checker:
|
|
|
3006
3160
|
scope.bind(s.name, "derive")
|
|
3007
3161
|
elif isinstance(s, A.LoadDecl):
|
|
3008
3162
|
scope.bind(s.name, "load result")
|
|
3163
|
+
self._ctx = "view"
|
|
3009
3164
|
self._scoped(it.body, [top, scope], fallible=True)
|
|
3165
|
+
self._ctx = "route"
|
|
3010
3166
|
elif isinstance(it, A.ServiceDecl):
|
|
3011
3167
|
for r in it.routes:
|
|
3012
3168
|
scope = _Scope((p.name for p in r.params), "route parameter")
|
|
@@ -3121,23 +3277,100 @@ class Checker:
|
|
|
3121
3277
|
def _check_builtin_call(self, call, scopes: list[set]) -> None:
|
|
3122
3278
|
"""A bare call to an allowlisted name the build cannot lower compiles
|
|
3123
3279
|
to an undefined JS function (action-48). Skipped when the name is the
|
|
3124
|
-
author's own: a local binding or a module declaration or
|
|
3280
|
+
author's own: a local binding or a module declaration or a
|
|
3281
|
+
non-`std` import."""
|
|
3125
3282
|
callee = call.callee
|
|
3126
|
-
if not isinstance(callee, A.Name):
|
|
3283
|
+
if not isinstance(callee, A.Name) or id(call) in self._exempt_calls:
|
|
3127
3284
|
return
|
|
3128
3285
|
nm = callee.id
|
|
3129
|
-
if nm in
|
|
3286
|
+
if nm in STD_BUILT_FNS:
|
|
3287
|
+
return
|
|
3288
|
+
# `session` / `request` are bound in a view's or handler's scope, but
|
|
3289
|
+
# they are the ambient objects, never the author's own function
|
|
3290
|
+
ambient = nm in _AMBIENT_RECEIVERS
|
|
3291
|
+
throws = "compiles to a call the built page or route cannot make, and throws when it runs"
|
|
3292
|
+
if (nm in self._views and self._emitted
|
|
3293
|
+
and not any(nm in s for s in scopes[1:])):
|
|
3294
|
+
# a view is rendered by a line of its own (an element); as a
|
|
3295
|
+
# value, `Row(1)` calls the emitted render function with no
|
|
3296
|
+
# context and throws
|
|
3297
|
+
self._d("E-NOT-A-FUNCTION", Severity.ERROR,
|
|
3298
|
+
"'%s' is a view, not a function -- as a value `%s(...)` %s"
|
|
3299
|
+
% (nm, nm, throws), call,
|
|
3300
|
+
"render it as its own line in the view body: `%s(...)`" % nm)
|
|
3301
|
+
return
|
|
3302
|
+
if not ambient and (nm in self._module_names
|
|
3303
|
+
or any(nm in s for s in scopes[1:])):
|
|
3130
3304
|
return
|
|
3131
3305
|
if nm in _METHOD_ONLY:
|
|
3132
3306
|
self._d("E-METHOD-AS-FN", Severity.ERROR,
|
|
3133
3307
|
"'%s' is a method, not a function -- `%s(...)` compiles to "
|
|
3134
3308
|
"an undefined function and throws when the page runs"
|
|
3135
3309
|
% (nm, nm), call, "call it on the value: `%s`" % _METHOD_ONLY[nm])
|
|
3136
|
-
elif
|
|
3310
|
+
elif not self._emitted:
|
|
3311
|
+
# a module-level `fn` body: the build refuses module functions
|
|
3312
|
+
# (E-VIEW-MODULE-FN), so nothing here is emitted
|
|
3313
|
+
return
|
|
3314
|
+
elif nm in _FN_NOT_BUILT:
|
|
3137
3315
|
self._d("E-FN-NOT-BUILT", Severity.ERROR,
|
|
3138
|
-
"'%s' is
|
|
3139
|
-
"the call compiles
|
|
3316
|
+
"'%s' is a std name this build of Volaro has no function "
|
|
3317
|
+
"for -- the call compiles to an undefined function and "
|
|
3318
|
+
"throws when it runs" % nm,
|
|
3140
3319
|
call, "use %s" % _FN_NOT_BUILT[nm])
|
|
3320
|
+
elif nm == "err":
|
|
3321
|
+
if self._ctx == "view":
|
|
3322
|
+
self._d("E-ERR-CONTEXT", Severity.ERROR,
|
|
3323
|
+
"`err(...)` fails a route; a view has no route to fail "
|
|
3324
|
+
"-- in a view it %s" % throws, call,
|
|
3325
|
+
"use " + _ERR_ROUTE_FIX)
|
|
3326
|
+
elif nm in _ERROR_CODES or nm in self._variants:
|
|
3327
|
+
what = ("the error code '%s'" % nm if nm in _ERROR_CODES
|
|
3328
|
+
and nm not in self._variants
|
|
3329
|
+
else "the union variant '%s'" % nm)
|
|
3330
|
+
self._d("E-ERR-CONTEXT", Severity.ERROR,
|
|
3331
|
+
"%s is built only as the argument of `err(...)` in a route "
|
|
3332
|
+
"body -- here `%s(...)` %s" % (what, nm, throws), call,
|
|
3333
|
+
"use " + _ERR_ROUTE_FIX + ("" if nm in _ERROR_CODES else
|
|
3334
|
+
"; for data, a record type with the same fields"))
|
|
3335
|
+
elif nm in _WHERE_PREDICATE_FIX:
|
|
3336
|
+
self._d("E-WHERE-PREDICATE", Severity.ERROR,
|
|
3337
|
+
"'%s' is a `where()` predicate (spec section 13.1), built "
|
|
3338
|
+
"only as a `.where(field: %s(...))` value -- here it %s"
|
|
3339
|
+
% (nm, nm, throws), call,
|
|
3340
|
+
"compare the values with %s, or move it into "
|
|
3341
|
+
"`.where(field: %s(x))`" % (_WHERE_PREDICATE_FIX[nm], nm))
|
|
3342
|
+
elif nm in _NOT_CALLABLE:
|
|
3343
|
+
self._d("E-NOT-A-FUNCTION", Severity.ERROR,
|
|
3344
|
+
"'%s' is not a function -- `%s(...)` %s" % (nm, nm, throws),
|
|
3345
|
+
call, "use %s" % _NOT_CALLABLE[nm])
|
|
3346
|
+
elif nm in ELEMENT_NAMES:
|
|
3347
|
+
self._d("E-NOT-A-FUNCTION", Severity.ERROR,
|
|
3348
|
+
"'%s' is an element, not a function -- as a value "
|
|
3349
|
+
"`%s(...)` %s" % (nm, nm, throws), call,
|
|
3350
|
+
"write the element as its own line in the view body: `%s ...`"
|
|
3351
|
+
% nm)
|
|
3352
|
+
|
|
3353
|
+
def _mark_positional_calls(self, call) -> None:
|
|
3354
|
+
"""Exempt the calls whose position gives them meaning: a predicate as
|
|
3355
|
+
a `.where(field: ...)` value (either branch of an `if` value too), and
|
|
3356
|
+
the error constructor that is the argument of `err(...)`."""
|
|
3357
|
+
callee = call.callee
|
|
3358
|
+
if isinstance(callee, A.Member) and callee.name == "where":
|
|
3359
|
+
stack = [v for _, v in call.named]
|
|
3360
|
+
while stack:
|
|
3361
|
+
v = stack.pop()
|
|
3362
|
+
if isinstance(v, A.IfExpr):
|
|
3363
|
+
stack += [v.then, v.orelse]
|
|
3364
|
+
elif (isinstance(v, A.Call) and isinstance(v.callee, A.Name)
|
|
3365
|
+
and v.callee.id in _WHERE_PREDICATE_FIX):
|
|
3366
|
+
self._exempt_calls.add(id(v))
|
|
3367
|
+
elif isinstance(callee, A.Name) and callee.id == "err":
|
|
3368
|
+
# in a view the `err(...)` itself is the one error reported
|
|
3369
|
+
for a in call.args:
|
|
3370
|
+
if (isinstance(a, A.Call) and isinstance(a.callee, A.Name)
|
|
3371
|
+
and (a.callee.id in _ERROR_CODES
|
|
3372
|
+
or a.callee.id in self._variants)):
|
|
3373
|
+
self._exempt_calls.add(id(a))
|
|
3141
3374
|
|
|
3142
3375
|
@staticmethod
|
|
3143
3376
|
def _value_receiver(obj, scopes: list[set]) -> bool:
|
|
@@ -3331,6 +3564,7 @@ class Checker:
|
|
|
3331
3564
|
self._expr(ex.obj, scopes, fallible)
|
|
3332
3565
|
self._expr(ex.index, scopes, fallible)
|
|
3333
3566
|
elif isinstance(ex, A.Call):
|
|
3567
|
+
self._mark_positional_calls(ex)
|
|
3334
3568
|
self._check_builtin_call(ex, scopes)
|
|
3335
3569
|
self._check_value_method(ex, scopes)
|
|
3336
3570
|
callee = ex.callee
|