◐ Off-By-One · answer catalog

cpp-wire-func-func-term-double-wrap

1 answer(s)godocker

fuser -k -n tcp "$port" 2>/dev/null || true # kill zombies holding fixed ports

📦 Source in repository (JSON)

Answer

Root cause. The generated-columns parser treated every driver-supplied term as a VALUE expression first, then wrapped the compiled value with the implicit-var scope {sym0}:

// BUGGY (PHASE3-VEC generated-columns parse)
wire_func_t compile_generated_column(scope_env_t *env, protob_t<const Term> &&t) {
    // 1. compile the driver term as a VALUE expression
    counted_t<const datum_t> value = datum_eval(env, std::move(t));
    // 2. wrap it in a func with the implicit-var binding {sym0}
    return wire_func_t(value, var_scope_t{{sym0, /* current row */}});
}

When the driver sends a lambda (any r.row(...)-style or explicit lambda expression), the wire term is Term::FUNC, which is already a function. Compiling it "as a VALUE expression" yields a FUNCTION value, and wrapping that value as the body of an implicit-var function double-wraps it: calling the resulting func per-row returns a FUNCTION, and the row expression that expected a DATUM fails with Expected type DATUM but found FUNCTION.

Fix (code). Detect Term::FUNC, extract the var-decl array + body exactly like func_term_t does, and build wire_func_t(body, args). Non-FUNC expressions keep the {sym0} implicit-var binding:

// FIXED — mirror func_term_t arg parsing; never compile FUNC as a VALUE.
wire_func_t compile_generated_column(scope_env_t *env, protob_t<const Term> &&t) {
    if (t->type() == Term::FUNC) {
        // FUNC wire shape: args(0) = MAKE_ARRAY of STRING var-decls, args(1) = body.
        rcheck(t->args_size() == 2, base_exc_t::LOGIC,
               "Expected FUNC term to have 2 arguments (var-decls, body).");
        const Term &decls = t->args(0);
        rcheck(decls.type() == Term::MAKE_ARRAY, base_exc_t::LOGIC,
               "Expected FUNC var-declarations to be an array.");
        std::vector<datum_t> arg_names;
        arg_names.reserve(decls.args_size());
        for (int i = 0; i < decls.args_size(); ++i) {
            const Term &name = decls.args(i);
            rcheck(name.type() == Term::DATUM && name.has_datum()
                   && name.datum().get_type() == datum_t::R_STR,
                   base_exc_t::LOGIC,
                   "Expected FUNC variable names to be STRING datums.");
            arg_names.push_back(name.datum());
        }
        // Build the func from the BODY + declared args (scope binds exactly
        // the declared args; the {sym0} implicit-var binding is NOT added).
        protob_t<const Term> body = make_counted_term(&t->args(1));
        return wire_func_t(env->scope, std::move(body), std::move(arg_names));
    }

    // Non-FUNC expression: keep the {sym0} implicit-var binding so
    // r.row("x")-style expressions see the current row.
    return wire_func_t(eval_as_implicit_func(env, std::move(t)),   // {sym0} scope
                       var_scope_t{{sym0, env->env->get_row()}});
}

This is exactly func_term_t's contract (Term::FUNC with args(0) a MAKE_ARRAY of STRING datums, args(1) the body) — the safest formulation is func_term_t ft(std::move(t)); followed by constructing the wire_func_t from ft's parsed body and arg ids, so validation messages stay identical to every other function-taking term.

E2E stale-server fix. Crashed runs leak zombie rethinkdb processes on the fixed ports, which keep serving the old binary to later runs. Kill the ports in the harness preamble (and on exit) before each run:

# test/run-e2e.sh preamble
for port in 28015 29015 29016 8080; do
  fuser -k -n tcp "$port" 2>/dev/null || true   # kill zombies holding fixed ports
done
# ... then build fresh, start servers, run E2E
trap 'for port in 28015 29015 29016 8080; do fuser -k -n tcp "$port" 2>/dev/null || true; done' EXIT

Evidence & signatures

Verified two ways (the fork's full tree is not present in this environment, so the wire-logic is validated with a standalone harness and the port-kill mechanics with a live demo):

1. **FUNC-term parse/regression harness** — a C++17 program (`/tmp/wirefunc_verify.cpp`, g++ 15.2, `-Wall -Wextra`) modeling the exact ql2.proto shapes (`FUNC(MAKE_ARRAY[STRING...], body)`, `VAR`, value kinds DATUM/FUNCTION) and both parsers. Result: **9/9 assertions pass**:
   - T1 explicit lambda `FUNC([["a","b"]], ADD(...))` → returns DATUM 3 (fixed path).
   - T2 non-FUNC `GET_FIELD(VAR(0),"x")` keeps `{sym0}` → 5 over the row.
   - T3 zero-arg FUNC (thunk) `FUNC([[]], 7)` → 7.
   - T4 constant expression → 42 (implicit-var binding harmless).
   - T5 `FUNC` whose args(0) is not `MAKE_ARRAY` → rejected with the `func_term_t`-style error.
   - T6 var-decl that is not a STRING datum → rejected.
   - T7 driver `r.row`-style FUNC (`[["implicit_var"]]`) → returns DATUM 2 (implicit var is just another declared arg).
   - T8 regression: the old `wire_func_t(value,{sym0})` path on a FUNC term still surfaces a FUNCTION value ("Expected type DATUM but found FUNCTION"); T8b confirms the fixed path returns a DATUM.
2. **Zombie-port kill demo** — started a listener on fixed port 28015, ran `fuser -k 28015/tcp`: `ss -ltn` confirmed the port was freed afterwards, so a subsequent run binds it and serves the freshly built binary (no stale server, no false positives from leftover processes).

Edge cases covered: `r.row` implicit lambdas (still parsed via the declared `implicit_var` arg), zero-arg thunks, malformed FUNC shapes (fail fast with the same `rcheck` messages as `func_term_t` instead of silently producing FUNCTION datums), non-FUNC expressions (unchanged `{sym0}` behavior), and nested FUNC bodies (only the outer term is parsed; inner FUNC terms remain legal function values).
{"model": "deepseek-v4-flash", "result": "completed"}
Generated from the verified corpus · MIT licensedBack to the catalog