// Resolving a module's conditionals against one configuration.
//
// Ported from `specialize_fields` in wax/src/lib-wax/typing.ml.
//
// A module with `#[if]` in it is not one program but a family of them, and
// type-checking it in place would ask questions no single answer fits: whether
// a value is left on the stack depends on which branch is compiled. So each
// reachable configuration is SPECIALIZED -- every conditional resolved, the
// selected branch spliced into the enclosing list -- and checked on its own.
//
// The assumption threads left to right through siblings, so once one
// conditional forces `$wasi` a later `#[if(not wasi)]` has its then-branch
// pruned rather than explored.
///|
/// What a specializer needs to resolve one conditional, gathered so the
/// recursive walk below can pass it around as one thing.
priv struct Configuring {
env : @cond.Env
diagnostics : @diagnostic.Context
/// Takes the complementary assumption of an undecided conditional -- the
/// configuration this one does not cover.
enqueue : (@cond.T) -> Unit
/// Accumulates the literals this configuration commits to, so the explorer
/// knows the full assumption every diagnostic was produced under.
record : (@cond.T) -> Unit
}
///|
/// Resolve one conditional: the selected branch, and the assumption that holds
/// after it.
///
/// A branch is taken only if it is REACHABLE -- its conjunction with the
/// current assumption is satisfiable -- so an infeasible configuration is never
/// explored. When neither branch is forced, this one takes the `then` and hands
/// the `else` configuration back to the explorer.
fn[A] Configuring::choose(
self : Configuring,
under : @cond.T,
cond : @wasm_types.Cond,
location : @basic.Location,
then_branch : (@cond.T) -> A,
else_branch : (@cond.T) -> A,
) -> (A, @cond.T) {
let c = self.env.of_cond(self.diagnostics, location, cond)
let then_under = @cond.and_(under, c)
let else_under = @cond.and_(under, @cond.not_(c))
if !@cond.is_satisfiable(then_under) {
(self.record)(@cond.not_(c))
(else_branch(else_under), else_under)
} else if !@cond.is_satisfiable(else_under) {
(self.record)(c)
(then_branch(then_under), then_under)
} else {
(self.enqueue)(else_under)
(self.record)(c)
(then_branch(then_under), then_under)
}
}
///|
/// Decide every attribute guard.
///
/// A guard gates the presence of just this attribute, so it partitions the
/// space exactly as an `#[if]` block does -- the configurations are explored
/// on it. The attribute itself is KEPT in every one: the reference checks an
/// attribute whatever its guard selects, so what is wrong with one (a guard
/// where none is allowed, a value of the wrong shape) is wrong in every
/// configuration rather than only where it is present.
///
/// An `export` or `start`, where a guard means something, says in each
/// configuration whether it is there: present, it loses its guard; absent, it
/// keeps one that never holds, so that it clashes with nothing.
fn Configuring::attrs(
self : Configuring,
under : @cond.T,
attrs : @ast.Attributes,
) -> (@ast.Attributes, @cond.T) {
let out : @ast.Attributes = []
let mut under = under
for a in attrs {
match a.attr_guard {
None => out.push(a)
Some(g) => {
let (kept, next) = self.choose(
under,
g.desc,
g.info,
_ => { ..a, attr_guard: None, },
_ => { ..a, attr_guard: Some({ ..g, desc: Or([]), }), },
)
out.push(
if a.attr_name == "export" || a.attr_name == "start" {
kept
} else {
a
},
)
under = next
}
}
}
(out, under)
}
///|
/// A statement-position `#[if]` is spliced away, so one instruction in can be
/// any number out.
fn Configuring::instr(
self : Configuring,
under : @cond.T,
i : @ast.Instr[@basic.Location],
) -> (Array[@ast.Instr[@basic.Location]], @cond.T) {
match i.desc {
// The conditional is still a STATEMENT of the configuration, whichever
// branch it selects -- an absent one included -- so it is kept as a
// no-op at its own span ahead of the branch spliced in. That is where
// code after a divergence is found dead: at the conditional, not at
// whatever follows it.
IfAnnotation(cond~, then_body~, else_body~) => {
let marker = { ..i, desc: Nop, }
let (branch, next) = self.choose(
under,
cond,
i.info,
u => self.instrs(u, then_body.desc),
u => {
match else_body {
Some(e) => self.instrs(u, e.desc)
None => []
}
},
)
([marker, ..branch], next)
}
desc =>
(
[
{
..i,
desc: desc.map_desc(instr=x => self.one(under, x), block=l => {
self.instrs(under, l)
}),
},
],
under,
)
}
}
///|
/// A list of statements, with the assumption threading through them.
fn Configuring::instrs(
self : Configuring,
under : @cond.T,
l : Array[@ast.Instr[@basic.Location]],
) -> Array[@ast.Instr[@basic.Location]] {
let out : Array[@ast.Instr[@basic.Location]] = []
let mut under = under
for i in l {
let (got, next) = self.instr(under, i)
for g in got {
out.push(g)
}
under = next
}
out
}
///|
/// A single-instruction position, where a conditional cannot appear -- it is
/// statement-only, so exactly one instruction comes back.
fn Configuring::one(
self : Configuring,
under : @cond.T,
i : @ast.Instr[@basic.Location],
) -> @ast.Instr[@basic.Location] {
match self.instr(under, i).0 {
[x] => x
_ => i
}
}
///|
fn Configuring::decl(
self : Configuring,
under : @cond.T,
d : @basic.Annotated[@ast.ImportDecl, @basic.Location],
) -> (@basic.Annotated[@ast.ImportDecl, @basic.Location], @cond.T) {
let (attributes, under) = self.attrs(under, d.desc.attributes)
({ ..d, desc: { ..d.desc, attributes, }, }, under)
}
///|
/// One module field, which a conditional group turns into any number.
fn Configuring::field(
self : Configuring,
under : @cond.T,
f : @basic.Annotated[@ast.ModuleField[@basic.Location], @basic.Location],
) -> (
Array[@basic.Annotated[@ast.ModuleField[@basic.Location], @basic.Location]],
@cond.T,
) {
match f.desc {
Conditional(cond~, then_fields~, else_fields~) =>
self.choose(under, cond, f.info, u => self.fields(u, then_fields.desc), u => {
match else_fields {
Some(e) => self.fields(u, e.desc)
None => []
}
})
Func(name~, typ~, sign~, body~, attributes~) => {
let (attributes, under) = self.attrs(under, attributes)
let (label, instrs) = body
(
[
{
..f,
desc: Func(
name~,
typ~,
sign~,
body=(label, self.instrs(under, instrs)),
attributes~,
),
},
],
under,
)
}
Global(name~, mut_~, typ~, def~, attributes~) => {
let (attributes, under) = self.attrs(under, attributes)
(
[
{
..f,
desc: Global(
name~,
mut_~,
typ~,
def=self.one(under, def),
attributes~,
),
},
],
under,
)
}
Tag(name~, typ~, sign~, attributes~) => {
let (attributes, under) = self.attrs(under, attributes)
([{ ..f, desc: Tag(name~, typ~, sign~, attributes~), }], under)
}
Memory(
name~,
address_type~,
limits~,
page_size_log2~,
shared~,
data~,
attributes~
) => {
let (attributes, under) = self.attrs(under, attributes)
(
[
{
..f,
desc: Memory(
name~,
address_type~,
limits~,
page_size_log2~,
shared~,
data~,
attributes~,
),
},
],
under,
)
}
Table(name~, address_type~, reftype~, limits~, init~, attributes~) => {
let (attributes, under) = self.attrs(under, attributes)
(
[
{
..f,
desc: Table(
name~,
address_type~,
reftype~,
limits~,
init~,
attributes~,
),
},
],
under,
)
}
Import(module_~, decl~) => {
let (decl, under) = self.decl(under, decl)
([{ ..f, desc: Import(module_~, decl~), }], under)
}
ImportGroup(module_~, decls~) => {
let out = []
let mut under = under
for d in decls {
let (d2, next) = self.decl(under, d)
out.push(d2)
under = next
}
([{ ..f, desc: ImportGroup(module_~, decls=out), }], under)
}
ModuleAnnotation(a) => {
let (a, under) = self.attrs(under, a)
([{ ..f, desc: ModuleAnnotation(a), }], under)
}
Type(_) | TypeAlias(..) | Data(..) | Elem(..) => ([f], under)
}
}
///|
/// A list of fields, with the assumption threading through them.
fn Configuring::fields(
self : Configuring,
under : @cond.T,
fl : @ast.Module[@basic.Location],
) -> @ast.Module[@basic.Location] {
let out : @ast.Module[@basic.Location] = []
let mut under = under
for f in fl {
let (got, next) = self.field(under, f)
for g in got {
out.push(g)
}
under = next
}
out
}
///|
/// Specialize `fields` under `assumption`, producing a conditional-free module.
fn specialize_fields(
env : @cond.Env,
diagnostics : @diagnostic.Context,
assumption : @cond.T,
fields : @ast.Module[@basic.Location],
enqueue : (@cond.T) -> Unit,
record : (@cond.T) -> Unit,
) -> @ast.Module[@basic.Location] {
({ env, diagnostics, enqueue, record, } : Configuring).fields(
assumption, fields,
)
}
///|
/// Whether anything in this module is conditional.
///
/// Gates the whole configuration machinery: a module with no `#[if]` anywhere
/// is one program, and exploring it would be a whole-module walk to discover
/// there is nothing to explore.
///
/// An attribute GUARD counts, as it does for the reference: it partitions the
/// configurations like an `#[if]`, so an export clash or an unused definition
/// is qualified by the guard like by any conditional.
fn module_has_conditional(fields : @ast.Module[@basic.Location]) -> Bool {
fn in_instr(i : @ast.Instr[@basic.Location]) -> Bool {
if i.desc is IfAnnotation(..) {
return true
}
i.sub_instrs().iter().any(in_instr)
}
fn guarded(attrs : @ast.Attributes) -> Bool {
attrs.iter().any(a => a.attr_guard is Some(_))
}
fn in_field(
f : @basic.Annotated[@ast.ModuleField[@basic.Location], @basic.Location],
) -> Bool {
match f.desc {
Conditional(..) => true
Func(body~, attributes~, ..) =>
guarded(attributes) || body.1.iter().any(in_instr)
Global(def~, attributes~, ..) => guarded(attributes) || in_instr(def)
Tag(attributes~, ..)
| Memory(attributes~, ..)
| Table(attributes~, ..)
| Data(attributes~, ..)
| Elem(attributes~, ..)
| ModuleAnnotation(attributes) => guarded(attributes)
Import(decl~, ..) => guarded(decl.desc.attributes)
ImportGroup(decls~, ..) =>
decls.iter().any(d => guarded(d.desc.attributes))
Type(_) | TypeAlias(..) => false
}
}
fields.iter().any(in_field)
}
///|
/// Report a `let` binding written inside a conditional branch.
///
/// Branches are transparent and mutually exclusive: a name bound in one would
/// leak past the conditional and clash with the other branch. An anonymous
/// `_ = e` binds nothing, so it is allowed.
fn check_let_bindings(
diagnostics : @diagnostic.Context,
fields : @ast.Module[@basic.Location],
) -> Unit {
fn branch(l : Array[@ast.Instr[@basic.Location]]) -> Unit {
for s in l {
if s.desc is Let(bindings, _) && bindings.iter().any(b => b.0 is Some(_)) {
let_in_conditional(diagnostics, s.info)
}
}
}
fn go(i : @ast.Instr[@basic.Location]) -> Unit {
if i.desc is IfAnnotation(then_body~, else_body~, ..) {
branch(then_body.desc)
if else_body is Some(b) {
branch(b.desc)
}
}
for sub in i.sub_instrs() {
go(sub)
}
}
fn in_field(
f : @basic.Annotated[@ast.ModuleField[@basic.Location], @basic.Location],
) -> Unit {
match f.desc {
Func(body~, ..) =>
for s in body.1 {
go(s)
}
Global(def~, ..) => go(def)
Conditional(then_fields~, else_fields~, ..) => {
for g in then_fields.desc {
in_field(g)
}
if else_fields is Some(e) {
for g in e.desc {
in_field(g)
}
}
}
_ => ()
}
}
for f in fields {
in_field(f)
}
}
///|
/// Report every conditional branch no configuration selects: one whose
/// condition cannot hold together with the conditions of the branches
/// enclosing it, or cannot hold at all.
///
/// A property of the module, not of any one configuration, so it is reported
/// directly rather than through the explorer. Only the outermost branch of a
/// dead nest is reported: a conditional inside a dead branch is never decided.
/// A per-attribute guard is a conditional with two empty sides -- present and
/// absent -- and is decided like any other.
fn report_dead_branches(
diagnostics : @diagnostic.Context,
fields : @ast.Module[@basic.Location],
) -> Unit {
let env = @cond.Env::new()
// Ill-formed conditions are reported where the configurations are explored.
let quiet = @diagnostic.collector(parent=Some(diagnostics))
fn decide(
under : @cond.T,
location : @basic.Location,
cond : @wasm_types.Cond,
has_else : Bool,
then_ : (@cond.T) -> Unit,
else_ : (@cond.T) -> Unit,
) -> Unit {
let c = env.of_cond(quiet, location, cond)
let then_under = @cond.and_(under, c)
let else_under = @cond.and_(under, @cond.not_(c))
if @cond.is_satisfiable(then_under) {
then_(then_under)
} else {
dead_branch(diagnostics, location, side=true)
}
if has_else {
if @cond.is_satisfiable(else_under) {
else_(else_under)
} else {
dead_branch(diagnostics, location, side=false)
}
}
}
fn guards(under : @cond.T, attrs : @ast.Attributes) -> Unit {
for a in attrs {
if a.attr_guard is Some(g) {
decide(under, g.info, g.desc, true, _ => (), _ => ())
}
}
}
fn instr(under : @cond.T, i : @ast.Instr[@basic.Location]) -> Unit {
match i.desc {
IfAnnotation(cond~, then_body~, else_body~) =>
decide(
under,
i.info,
cond,
else_body is Some(_),
u => {
for x in then_body.desc {
instr(u, x)
}
},
u => {
if else_body is Some(e) {
for x in e.desc {
instr(u, x)
}
}
},
)
_ =>
for x in i.sub_instrs() {
instr(under, x)
}
}
}
fn instrs(under : @cond.T, l : Array[@ast.Instr[@basic.Location]]) -> Unit {
for i in l {
instr(under, i)
}
}
fn go(under : @cond.T, l : @ast.Module[@basic.Location]) -> Unit {
for f in l {
match f.desc {
Conditional(cond~, then_fields~, else_fields~) =>
decide(
under,
f.info,
cond,
else_fields is Some(_),
u => go(u, then_fields.desc),
u => if else_fields is Some(e) { go(u, e.desc) },
)
Func(body~, attributes~, ..) => {
guards(under, attributes)
instrs(under, body.1)
}
Global(def~, attributes~, ..) => {
guards(under, attributes)
instr(under, def)
}
Tag(attributes~, ..)
| Memory(attributes~, ..)
| Table(attributes~, ..)
| Data(attributes~, ..)
| Elem(attributes~, ..)
| ModuleAnnotation(attributes) => guards(under, attributes)
Import(decl~, ..) => guards(under, decl.desc.attributes)
ImportGroup(decls~, ..) =>
for d in decls {
guards(under, d.desc.attributes)
}
Type(_) | TypeAlias(..) => ()
}
}
}
go(@cond.true_, fields)
}
///|
/// Record the aliases defined under a conditional annotation, by name.
fn record_conditional_aliases(
out : Set[String],
fields : @ast.Module[@basic.Location],
under : Bool,
) -> Unit {
for f in fields {
match f.desc {
TypeAlias(name~, ..) => if under { out.add(name.name) }
Conditional(then_fields~, else_fields~, ..) => {
record_conditional_aliases(out, then_fields.desc, true)
if else_fields is Some(e) {
record_conditional_aliases(out, e.desc, true)
}
}
_ => ()
}
}
}
///|
/// Record every function body as written, for the source-shape lints of a
/// configuration (see `ModuleContext::preserved_bodies`).
fn record_preserved_bodies(
ctx : @typing_env.ModuleContext,
fields : @ast.Module[@basic.Location],
) -> Unit {
for f in fields {
match f.desc {
Func(body~, ..) =>
ctx.preserved_bodies[(f.info.start.cnum, f.info.end.cnum)] = body.1
Conditional(then_fields~, else_fields~, ..) => {
record_preserved_bodies(ctx, then_fields.desc)
if else_fields is Some(e) {
record_preserved_bodies(ctx, e.desc)
}
}
_ => ()
}
}
}
///|
/// Check every reachable configuration of a conditional module.
///
/// Each is specialized to be conditional-free and checked independently, in its
/// own collector, so a diagnostic is reported once -- annotated with the
/// assumption under which it is reachable, rather than repeated per
/// configuration that happened to produce it. Only the diagnostics matter here;
/// the typed module each configuration produces is thrown away.
///
/// A fresh type store per configuration, because a configuration declares its
/// own types: the branches may not agree on what a name means, and one
/// configuration's registrations must not be visible to the next.
fn check_configurations(
diagnostics : @diagnostic.Context,
store : @type_store.TypeStore,
features : @feature.Set,
fields : @ast.Module[@basic.Location],
simplify~ : Bool = false,
warn_unused~ : Bool = false,
lowering~ : Bool = false,
suggest~ : Bool = false,
) -> Unit {
ignore(store)
if warn_unused {
report_dead_branches(diagnostics, fields)
}
// Each configuration is checked on its own; what holds in each but not as
// one module -- the form a construct takes differing between them -- is the
// lowering's to report, once.
let forms : Map[(Int32, Int32), (@basic.Location, ConfigurationForm)]? = if lowering &&
!simplify {
Some(Map([]))
} else {
None
}
@cond_explore.check_all(
diagnostics,
specialize=(env, assumption, enqueue, record) => {
specialize_fields(env, diagnostics, assumption, fields, enqueue, record)
},
check=(cctx, cfg) => {
let ctx = @typing_env.ModuleContext::new(
cctx,
@type_store.TypeStore::new(),
features,
simplify~,
warn_unused~,
suggest~,
)
record_preserved_bodies(ctx, fields)
record_conditional_aliases(ctx.conditional_alias_names, fields, false)
declare_fields(ctx, cfg)
check_attributes(ctx, cfg)
let typed = check_fields(ctx, Operands::new(), cfg)
if forms is Some(tbl) {
record_configuration_forms(ctx, tbl, typed)
}
check_unused_aliases(ctx.type_context, ctx.diagnostics)
warn_unused_fields(ctx, cfg)
},
truncation_location=match fields {
[hd, ..] => Some(hd.info)
[] => None
},
explain=Some((env, c) => env.explain(c, style=Wax)),
)
if forms is Some(tbl) {
report_configuration_forms(diagnostics, tbl)
}
}
///|
/// What a construct lowers to in one configuration, for one whose form may
/// depend on the configuration: a cast to a conditional alias (its target
/// type), and a struct field access (the receiver's struct type and the
/// field's position).
priv enum ConfigurationForm {
LiteralForm(@ast.Ident, Array[String])
CastForm(@ast.Ident, Array[String])
FieldForm(@ast.Ident, Array[(String, Int32)])
}
///|
/// Record, by span, the form each such construct of `typed` takes in this
/// configuration.
///
/// A module lowered as one for every configuration takes each form from one
/// run, so `report_configuration_forms` rejects a construct the configurations
/// disagree on. Recorded by the checking runs, one per configuration, rather
/// than by the build, which only covers each branch once.
fn record_configuration_forms(
ctx : @typing_env.ModuleContext,
tbl : Map[(Int32, Int32), (@basic.Location, ConfigurationForm)],
typed : Array[
@basic.Annotated[
@ast.ModuleField[@typing_env.InferredAnnotation],
@basic.Location,
],
],
) -> Unit {
// Keyed by `l`, reported at `at`: a negative literal is reported whole.
fn add_at(
l : @basic.Location,
at : @basic.Location,
form : (ConfigurationForm?) -> ConfigurationForm,
) -> Unit {
let key = (l.start.cnum, l.end.cnum)
match tbl.get(key) {
Some((at_, prev)) =>
tbl[key] = (
if at_.start.cnum <= at.start.cnum {
at_
} else {
at
},
form(Some(prev)),
)
None => tbl[key] = (at, form(None))
}
}
fn add(
l : @basic.Location,
form : (ConfigurationForm?) -> ConfigurationForm,
) -> Unit {
add_at(l, l, form)
}
fn conditional_alias(a : @ast.Ident) -> Bool {
match ctx.type_context.aliases.find_no_mark(a.name) {
Some(al) => al.conditional
None => false
}
}
// A literal or null typed by a conditional alias: its type, printed.
fn typed_by(
l : @basic.Location,
at : @basic.Location,
cell : @infer.Cell[@infer.InferredType],
) -> Unit {
match cell.get() {
Valtype({ type_alias: Some(a), .. }) if conditional_alias(a) => {
let t = @infer.to_string(cell)
add_at(l, at, prev => {
match prev {
Some(LiteralForm(_, seen)) =>
LiteralForm(a, if seen.contains(t) { seen } else { [..seen, t] })
_ => LiteralForm(a, [t])
}
})
}
_ => ()
}
}
fn go(i : @ast.Instr[@typing_env.InferredAnnotation]) -> Unit {
let l = i.info.1
match i.desc {
Int(_) | Float(_) | Null if i.info.0 is [cell] => typed_by(l, l, cell)
UnOpI({ desc: Neg, .. }, { desc: Int(_) | Float(_), info: (lit, ll), .. }) if lit
is [cell] => typed_by(ll, l, cell)
Cast(_, Value(Alias(a))) if conditional_alias(a) =>
match i.info.0 {
[cell] =>
if cell.get() is Valtype(_) {
let t = @infer.to_string(cell)
add(l, prev => {
match prev {
Some(CastForm(_, seen)) =>
CastForm(
a,
if seen.contains(t) {
seen
} else {
[..seen, t]
},
)
_ => CastForm(a, [t])
}
})
}
_ => ()
}
StructGet(recv, f) | StructSet(recv, f, _) =>
match recv.info.0 {
[cell] =>
match cell.get() {
Valtype({ typ: Ref({ typ: Type(ty) | Exact(ty), .. }), .. }) =>
match ctx.type_context.types.find_no_mark(ty.name) {
Some((_, { typ: Struct(fields), .. })) => {
let mut index = -1
for k, fd in fields {
if index < 0 && fd.desc.0.name == f.name {
index = k
}
}
if index >= 0 {
let v = (ty.name, index)
add(l, prev => {
match prev {
Some(FieldForm(_, seen)) =>
FieldForm(
f,
if seen.contains(v) {
seen
} else {
[..seen, v]
},
)
_ => FieldForm(f, [v])
}
})
}
}
_ => ()
}
_ => ()
}
_ => ()
}
_ => ()
}
for x in i.sub_instrs() {
go(x)
}
}
fn fields(
l : Array[
@basic.Annotated[
@ast.ModuleField[@typing_env.InferredAnnotation],
@basic.Location,
],
],
) -> Unit {
for f in l {
match f.desc {
Func(body~, ..) =>
for x in body.1 {
go(x)
}
Global(def~, ..) => go(def)
Conditional(then_fields~, else_fields~, ..) => {
fields(then_fields.desc)
if else_fields is Some(e) {
fields(e.desc)
}
}
_ => ()
}
}
}
fields(typed)
}
///|
/// Report, once, each construct with no form that holds in every
/// configuration.
///
/// A field access is fine when the field is at the same position in every
/// type the receiver has: the lowering's `struct.get` of one is then valid on
/// the others, and a type redefined per configuration is named, so its field
/// resolves in each.
fn report_configuration_forms(
diagnostics : @diagnostic.Context,
tbl : Map[(Int32, Int32), (@basic.Location, ConfigurationForm)],
) -> Unit {
let entries : Array[(@basic.Location, ConfigurationForm)] = []
for _, e in tbl {
entries.push(e)
}
entries.sort_by((a, b) => a.0.start.cnum - b.0.start.cnum)
for e in entries {
let (location, form) = e
match form {
LiteralForm(a, seen) if seen.length() >= 2 =>
conditional_alias_literal(diagnostics, location, a.name)
CastForm(a, seen) if seen.length() >= 2 =>
conditional_alias_cast(diagnostics, location, a.name)
FieldForm(f, seen) if seen
.iter()
.any(v => seen.iter().any(w => v.0 != w.0 && v.1 != w.1)) =>
configuration_dependent_field(diagnostics, location, f.name)
_ => ()
}
}
}