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. 10 import Base 11 import ../../src.bend as Src 12 import ../../finding.bend as F 13 import ../../syntax/lex.bend as Lex 14 import ../../syntax/tree.bend as Tree 15 import ../../syntax/bind.bend as Bind 16 import ../../lazy/lazy.bend as Lazy 17 18 # which lanes a foreign def's bodies cover 19 type Lanes is Data: 20 Lanes{c: Bool, js: Bool} 21 22 # the imported path of an import statement's tokens (`"./x.c"`, quoted) 23 def 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 33 def 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 44 def 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) 53 def 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 64 def 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 68 def name.of(mm: Maybe<&2, String>) -> String: 69 match mm: 70 case None{}: 71 "?" 72 case Some{n}: 73 n 74 75 def 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 85 def 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? 95 def native_only(text: String) -> Bool: 96 native_only.go(String.lines(text)) 97 98 # the rule 99 def 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)