8000 codegen: a public function's argument check returns an incompatible empty struct · Issue #201 · google/wuffs · GitHub
[go: up one dir, main page]

Skip to content

codegen: a public function's argument check returns an incompatible empty struct #201

Open

Description

@dio-gh

Hi,

General context of this work is in #188, including the AI-disclaimers.

No in-tree codec is affected by this issue; std compiles, so no std/ package has a public non-coroutine method with a run-time-checked argument and a non-empty result. Claude judges the prospective risk of it as "low": only ever a compile error (an empty struct returned where a value is required), with no unsafe binary.

The following is Sol 6.1's writeup of the issue. Includes a repro, the generated C, and the MSVC C2440 return-type error, with a passing control.


For a public non-coroutine method with a run-time-checked argument (here a refined integer) and a non-empty result type, the generated argument check returns wuffs_base__make_empty_struct() whatever the declared result type. wuffs-c gen accepts the method, but the generated C does not compile.

Minimal repro:

pub struct thing?()

pub func thing.f!(x: base.u32[..= 1]) base.u32 {
    return 0
}

Actual: at 4be865da, wuffs-c gen -package_name repro repro.wuffs exits 0 with nothing on stderr. The generated function includes:

WUFFS_BASE__MAYBE_STATIC uint32_t
wuffs_repro__thing__f(
    wuffs_repro__thing* self,
    uint32_t a_x) {
  // Receiver checks omitted here.
  if (a_x > 1) {
    self->private_impl.magic = WUFFS_BASE__DISABLED;
    return wuffs_base__make_empty_struct();
  }
  return 0u;
}

Compiling it as C with MSVC (cl /nologo /c on a file containing #define WUFFS_IMPLEMENTATION and #include "repro.c", with the output of wuffs-c gen -package_name base saved as wuffs-base.c next to repro.c) exits 2:

repro.c(256): error C2440: 'return': cannot convert from 'wuffs_base__empty_struct' to 'uint32_t'

Control: replacing only base.u32[..= 1] with base.u32 removes the run-time check; generation and compilation both exit 0. The ? receiver has the magic field, so this repro does not involve the related report on public methods of non-? structs (#202).

Expected: accepted code generates compilable C, returning a value of the declared result type when an argument is invalid.

Cause: writeFuncImplPrologue calls the public argument validator:

wuffs/internal/cgen/func.go

Lines 422 to 427 in 4be865d

// For public functions, check (at runtime) the other args for bounds and
// null-ness. For private functions, those checks are done at compile time.
if g.currFunk.astFunc.Public() {
if err := g.writeFuncImplArgChecks(b, g.currFunk.astFunc); err != nil {
return err
}

writeFuncImplArgChecks collects the bounds checks:

wuffs/internal/cgen/func.go

Lines 576 to 622 in 4be865d

func (g *gen) writeFuncImplArgChecks(b *buffer, n *a.Func) error {
checks := []string(nil)
for _, o := range n.In().Fields() {
o := o.AsField()
oTyp := o.XType()
// TODO: Also check elements, for array-typed arguments.
switch {
case oTyp.IsIOTokenType() || (oTyp.Decorator() == t.IDPtr):
checks = append(checks, fmt.Sprintf("!%s%s", aPrefix, o.Name().Str(g.tm)))
case oTyp.IsRefined():
bounds := [2]*big.Int{}
for i, bound := range oTyp.Bounds() {
if bound != nil {
if cv := bound.ConstValue(); cv != nil {
bounds[i] = cv
}
}
}
if qid := oTyp.QID(); qid[0] == t.IDBase {
if key := qid[1]; key < t.ID(len(numTypeBounds)) {
ntb := numTypeBounds[key]
for i := 0; i < 2; i++ {
if bounds[i] != nil && ntb[i] != nil && bounds[i].Cmp(ntb[i]) == 0 {
bounds[i] = nil
continue
}
}
}
}
for i, bound := range bounds {
if bound != nil {
op := '<'
if i != 0 {
op = '>'
}
checks = append(checks, fmt.Sprintf("%s%s %c %s", aPrefix, o.Name().Str(g.tm), op, bound))
}
}
}
}
if len(checks) == 0 {
return nil
}

Lines AAD8 635-637 unconditionally return an empty struct for non-coroutines, whatever the result type; the TODO acknowledges this:

wuffs/internal/cgen/func.go

Lines 624 to 639 in 4be865d

b.writes("if (")
for i, c := range checks {
if i != 0 {
b.writes(" || ")
}
b.writes(c)
}
b.writes(") {\n")
b.writes("self->private_impl.magic = WUFFS_BASE__DISABLED;\n")
if g.currFunk.astFunc.Effect().Coroutine() {
b.writes("return wuffs_base__make_status(wuffs_base__error__bad_argument);\n")
} else {
// TODO: don't assume that the return type is empty.
b.printf("return wuffs_base__make_empty_struct();\n")
}
b.writes("}\n")

Both defects at once: with a plain receiver, pub struct thing(value: base.u32), and the same method, wuffs-c gen -package_name both both.wuffs exits 0 and MSVC reports both (exit 2):

both.c(175): error C2039: 'magic': is not a member of '<unnamed-tag>'
both.c(179): error C2039: 'magic': is not a member of '<unnamed-tag>'
both.c(180): error C2440: 'return': cannot convert from 'wuffs_base__empty_struct' to 'uint32_t'

Correcting either assumption alone leaves the other error.

Suggested fix: return a value of the declared result type in that branch, as the receiver check in the same prologue already does through writeOutParamZeroValue (internal/cgen/func.go:380-410).

Activity

  1. dio-gh commented on Nov 3, 2026

    @dio-gh
    Author

    Hi,

    This is a related case of this issue's argument check; the general context of this work, including the AI-disclaimers, is in #188.

    No in-tree codec is affected: std has no receiver-less public func, and none of its 125 pure public methods takes an argument that needs a run-time check. Claude judges the prospective risk of it as "low": the generated C fails to compile.

    The following is Sol 6.1's writeup of the issue. Includes a receiver-less repro and its generated C, two pure-method variants, MSVC C2065, C2223, C2166 and C2440 diagnostics, and two compiling controls. The cause is reasoned from source inspection; the repros and controls were compiled, not run.


    A public func's run-time argument check writes self->private_impl.magic = WUFFS_BASE__DISABLED; on failure. A receiver-less func has no self, and a pure method's self points to a const receiver. Both cases generate C that fails to compile.

    Minimal repro:

    pub struct repro?()
    
    pub func poke(x: base.u32[..= 1]) {
    	var a : array[2] base.u8
    
    	a[args.x] = 0x42
    }
    

    Actual: at b36a81d3, wuffs-c gen -package_name repro repro.wuffs exits 0 and emits:

    WUFFS_BASE__MAYBE_STATIC wuffs_base__empty_struct
    wuffs_repro__poke(uint32_t a_x) {
      if (a_x > 1) {
        self->private_impl.magic = WUFFS_BASE__DISABLED;
        return wuffs_base__make_empty_struct();
      }
      uint8_t v_a[2] = {0};
      v_a[a_x] = 66u;
      return wuffs_base__make_empty_struct();
    }

    MSVC rejects the amalgamation:

    release.c(26146): error C2065: 'self': undeclared identifier
    release.c(26146): error C2223: left of '->private_impl' must point to struct/union
    

    As a pure method, pub func repro.check(x: base.u32[..= 1]), the same body gets const wuffs_repro__repro* self and fails on the store:

    release.c(26163): error C2166: l-value specifies const object
    

    With a non-empty result, pub func repro.ident(x: base.u32[..= 1]) base.u32 { return args.x }, the pure method also produces this issue's C2440:

    release.c(26163): error C2166: l-value specifies const object
    release.c(26164): error C2440: 'return': cannot convert from 'wuffs_base__empty_struct' to 'uint32_t'
    

    Controls: the same body as an impure method, pub func repro.poke!(x: base.u32[..= 1]), compiles. A receiver-less func whose argument needs no run-time check also compiles.

    Expected: an argument check should not write to a missing or const receiver, and should return the declared result type. Alternatively, the checker could reject run-time-checked arguments on such funcs.

    Cause: writeFuncImplArgChecks emits the disabling store for every public func with an emitted argument check:

    wuffs/internal/cgen/func.go

    Lines 624 to 639 in b36a81d

    b.writes("if (")
    for i, c := range checks {
    if i != 0 {
    b.writes(" || ")
    }
    b.writes(c)
    }
    b.writes(") {\n")
    b.writes("self->private_impl.magic = WUFFS_BASE__DISABLED;\n")
    if g.currFunk.astFunc.Effect().Coroutine() {
    b.writes("return wuffs_base__make_status(wuffs_base__error__bad_argument);\n")
    } else {
    // TODO: don't assume that the return type is empty.
    b.printf("return wuffs_base__make_empty_struct();\n")
    }
    b.writes("}\n")

    These receiver errors come from the same failure branch as this issue's incompatible empty-struct return.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0