src/rules/correctness/foreign.bend
bolt@1.19.0.0 · 101 lines · 0 definitions
1# rule foreign: a foreign def (its body only `import "./x.c"` / `import
2# "./x.js"` lines) with a body for one lane and not the other. Each lane
3# finds out only when it first runs: a native build (`bend -o bin`) fails
4# with "no .c import", an interpreted or JS run with "a foreign def without a
5# .js import". Give every foreign def both bodies, `x.c` and `x.js`. A file
6# headed `# lanes: native` never runs on the JS lane, so it needs no `.js`.
7# Headed means the exact line `# lanes: native` is one of the file's leading
8# comment lines, those before its first line that does not start with `#`;
9# the line anywhere later does not count.
10import Base
11import bolt@1.19.0.0/src/src.bend as Src
12import bolt@1.19.0.0/src/finding.bend as F
13import bolt@1.19.0.0/src/syntax/lex.bend as Lex
14import bolt@1.19.0.0/src/syntax/tree.bend as Tree
15import bolt@1.19.0.0/src/syntax/bind.bend as Bind
16import bolt@1.19.0.0/src/lazy/lazy.bend as Lazy
17
18# which lanes a foreign def's bodies cover
19type Lanes is Data:
20 Lanes{c: Bool, js: Bool}
21
22# the imported path of an import statement's tokens (`"./x.c"`, quoted)
23def target(kids: Tree.Node) -> String:
24 match kids:
25 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TStr{}, t, l, c}}, rest}:
26 t
27 case Tree.NCons{h, rest}:
28 target(rest)
29 case other:
30 ""
31
32# the lanes of a body that is only imports; None when it is anything else
33def lanes.go(body: Tree.Node, cc: Bool, js: Bool) -> Maybe<&2, Lanes>:
34 match body:
35 case Tree.NCons{Tree.Stmt{Tree.SImport{}, kids, ib}, rest}:
36 +t = target(kids)
37 lanes.go(rest, Bool.or(cc, String.ends_with(t, ".c\"")), Bool.or(js, String.ends_with(t, ".js\"")))
38 case Tree.NCons{h, rest}:
39 None{}
40 case other:
41 Some{Lanes{cc, js}}
42
43# the lanes of a def's body: None unless it is a foreign def
44def lanes(body: Tree.Node) -> Maybe<&2, Lanes>:
45 match body:
46 case Tree.NCons{Tree.Stmt{Tree.SImport{}, kids, ib}, rest}:
47 +t = target(kids)
48 lanes.go(rest, String.ends_with(t, ".c\""), String.ends_with(t, ".js\""))
49 case other:
50 None{}
51
52# what the lanes lack, as a message ("" for nothing)
53def lack(mm: Maybe<&2, Lanes>, +native: Bool, +name: String) -> String:
54 match mm:
55 case None{}:
56 ""
57 case Some{Lanes{+c, +js}}:
58 Bool.pick(String, Bool.and(c, Bool.and(Bool.not(js), Bool.not(native))),
59 "Foreign def " ++ name ++ " has a .c body but no .js body, so it cannot run on the JS lane.",
60 Bool.pick(String, Bool.and(js, Bool.not(c)),
61 "Foreign def " ++ name ++ " has a .js body but no .c body, so it cannot run in a native build.", ""))
62
63# a message as a finding, when there is one
64def at(+msg: String, +line: U32, +col: U32, +path: String, +more: List<&2, F.Finding>) -> List<&2, F.Finding>:
65 Bool.pick(List<&2, F.Finding>, String.is_empty(msg), more, F.Finding{path, line, col, 0, "foreign", msg} <> more)
66
67# a def's name, `?` when it has none
68def name.of(mm: Maybe<&2, String>) -> String:
69 match mm:
70 case None{}:
71 "?"
72 case Some{n}:
73 n
74
75def check.go(root: Tree.Node, +native: Bool, +path: String) -> List<&2, F.Finding>:
76 match root:
77 case Tree.NCons{Tree.Stmt{Tree.SDef{}, +kids, body}, rest}:
78 at(lack(lanes(body), native, name.of(Bind.declared(kids))), Tree.line(kids), Tree.col(kids), path,
79 check.go(rest, native, path))
80 case Tree.NCons{h, rest}:
81 check.go(rest, native, path)
82 case other:
83 Nil{}
84
85def native_only.go(ls: List<&2, String>) -> Bool:
86 match ls:
87 case Nil{}:
88 False{}
89 case Con{+l, rest}:
90 Lazy.stop(Bool, Bool.not(String.starts_with(l, "#")), False{},
91 _u => Lazy.or_else(String.eq(l, "# lanes: native"), _v => native_only.go(rest)))
92
93# is the file headed `# lanes: native`: that exact line among the comment
94# lines before its first line that is not a comment?
95def native_only(text: String) -> Bool:
96 native_only.go(String.lines(text))
97
98# the rule
99def check(ss: Src.Src) -> List<&2, F.Finding>:
100 Src.Src{path, text, toks, tree, bound, items} = ss
101 check.go(tree, native_only(text), path)