Skip to content

DEP003/DEP004: optional calls (fn?.()) to known-effect externals — should the outer fn declare conditional effects? #110

@marcelofarias

Description

@marcelofarias

Problem

The DEP003/DEP004 over-declaration warning system suppresses when a fn has an opaque external call (i.e., a call to a function not listed in fnNames or moduleEffects). This is correct for truly unknown externals.

But there's an unresolved case: known-signature external functions called optionally via fn?.().

Concrete Case

?bs 0.9

// moduleEffects.json: { "reportService": { "reads": ["network"] } }
fn runner() reads { cache, network } -> void {
  cache.update()         // reads cache — justified via same-file callee
  reportService?.()      // optional call to known-effect external
}

Here reportService has a known reads { network } surface via moduleEffects, but the call is optional. Three positions on whether runner should declare reads { network }:

  1. Yes, always (conservative) — declared effect surface means "possible effects," not "guaranteed effects." The compiler cannot prove reportService?.() never runs, so the declaration is required. This is the current implicit stance for non-optional calls.

  2. No when optional — if the call doesn't execute at runtime, no effect happened. Requires the compiler to track call optionality through the call graph, which adds significant complexity.

  3. New syntaxreads? { network } for conditional/optional effects. Precise but adds annotation surface area and creates questions about how conditional effects compose transitively.

Current Behavior

The current hasOpaqueCall implementation (as of the fix in PR #100) treats fn?.() for unknown externals as opaque, suppressing DEP003/DEP004. For known externals (in moduleEffects) called via ?., the behavior is the same as a direct call — the outer fn must declare the full effect surface.

This is position 1 by default, but without it being an explicit design decision.

Questions

  1. Is position 1 (conservative, always declare) the right default for ?bs 0.9?
  2. Should optional call detection be extended to the collectCallees pass so that fn?.() to a known callee is treated differently from fn()?
  3. Is there a practical use case where position 2 or 3 is meaningfully better?

Relationship to Existing Codes

  • DEP001/DEP002 (under-declared reads/writes) — callers of fns with declared effects must propagate those declarations
  • DEP003/DEP004 (over-declared reads/writes) — over-declared effects are warned; suppressed for opaque calls
  • The optional-call question sits at the intersection: a conditionally-called known external sits between "opaque" (suppress) and "fully tracked" (enforce)

Metadata

Metadata

Assignees

No one assigned

    Labels

    compilerCompiler / transform passdiscussionDesign discussion or RFC

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions