Skip to content

Calling a method through a pointer or reference, or on another call's result, generates C, C++ and Rust that doesn't compile #170

Open

Description

@matt-edmondson

What's wrong

CallExpression.Receiver is written the same way whatever the receiver's type or value category:

C maps both TypeIndirection.Reference and TypeIndirection.Pointer to * (L1109-1122). Rust maps Pointer to a raw *const T / *mut T (RustGenerator.cs L1616-1630), and raw pointers don't auto-deref for method calls.

Reproduction (at 1d544a3)

The fixture is a struct Vec with these methods:

  • double length() const
  • Vec twice() const
  • double dist(const Vec& other) const { return other.length(); }
  • static double chain(Vec v) { return v.twice().length(); }

The C output was compiled with gcc 13.3 (-std=c11 -Wall -Wextra -pedantic) and the C++ output with g++ -std=c++20:

  • C, dist: return Vec_length(&other); passes a const Vec** where a const Vec* is expected. gcc 13 warns with -Wincompatible-pointer-types; GCC 14+ makes this an error by default.
  • C, chain: return Vec_length(&Vec_twice(&v)); fails with error: lvalue required as unary '&' operand. No method chain, and no call on a function result, can compile in C.
  • C++, dist with a Pointer parameter: return other.length(); fails with error: request for member 'length' in 'other', which is of pointer type 'const Vec*' (maybe you meant to use '->' ?).
  • Rust, same Pointer parameter: other.length() on a *const Vec has no such method. This follows from the type mapping; it was not compiled.

Why it matters

CLAUDE.md says the receiver is modelled because languages disagree about it (a.b(c) versus b(&a, c)), but the lowering only handles a by-value lvalue receiver. Taking const T& parameters is the normal C++ idiom, and the C generator's own receiver is a T* self. Any method call on a reference or pointer parameter, and any method chain, produces C that doesn't compile.

This is distinct from #141, which is about having no node for the current instance. This issue is about ordinary parameters and call results.

Suggested fix

  • Have the C, C++ and Rust generators push the static-type scope that already exists in StandardLanguageGenerator, so the receiver's indirection is known. Today only GoGenerator pushes it.
  • C:
    • Omit the & when the receiver's static type is already a pointer or reference.
    • For a non-lvalue receiver (CallExpression, BinaryExpression, ConditionalExpression, a literal), hoist it into a temporary or emit a WriteInexpressible note.
  • C++: emit -> when the receiver's static type is a Pointer.
  • Rust: emit unsafe { (*p).m() }, or an agreed alternative, when the receiver is a Pointer.

Acceptance criteria

CGeneratedSourceCompilesTests, CppGeneratedSourceCompilesTests and RustGeneratedSourceCompilesTests each gain three cases, and all of them compile:

  • a call on a reference parameter
  • a call on a pointer parameter
  • a chained call, v.twice().length()

Activity

  1. matt-edmondson commented on Oct 6, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions