Skip to content

Commit 4cb8188

Browse files
committed
Attribute .= new initializers, Lock::Async identity, general T(x) coercion
Three general fixes surfaced while getting Cro::HTTP::Client's request pipeline to compose under rakupp (`has Lock::Async $!lock .= new` and friends): - `.= new` attribute initializers were silently DROPPED by the parser — only `= EXPR` defaults were captured, so `has T $!x .= new` left `$!x` the bare type object for every type. Parse `.= meth(args)` into a default of `$!x.meth(args)`, and pre-seed each attr slot with its typed/zero default before evaluating the default expression, so a self-referential `.= new` reads the type object as its invocant. Plain `= EXPR` defaults are unchanged. - Lock::Async now keeps its own type identity (so a `Lock::Async $!l` container accepts `Lock::Async.new`, which previously came back typed `Lock` and failed the container's type check) while sharing Lock's method implementations under the cooperative GIL (.protect/.lock/.unlock). - General `T(x)` type-coercion call: a known type used as a routine now coerces its argument through the argument's `.T` method (Supply($chan) == $chan.Supply, user-defined `.T` coercions), instead of "Undefined routine 'T'" for types outside the Int/Str/... fast path. Gate 194975 (+8), S17 unchanged at baseline, 0 regressions. Regression tests: dot-equals-new-attr-default.raku, type-coercion-call.raku. (Supply.Promise, needed to coerce `Promise(supply {…})`, is deferred: a naive tapSupply-based impl double-fired `done` on an already-tapped live Supply and regressed S17-supply/head.t. Cro's request still fails deeper — silently, at connection setup — so it is not currently gated on Supply.Promise.)
1 parent ccf3d12 commit 4cb8188

5 files changed

Lines changed: 113 additions & 19 deletions

File tree

src/Builtins.cpp

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2751,7 +2751,10 @@ Value Interpreter::methodCall(Value inv, const std::string& m, ValueList args, c
27512751
if (parallelMode_) { auto st = std::make_shared<SemaphoreState>(); st->count = n; v.ext = st; }
27522752
}
27532753
else {
2754-
v.hashKind = "Lock";
2754+
// Lock::Async keeps its own type identity (so a `Lock::Async $!l`
2755+
// container accepts it) but shares Lock's method implementations
2756+
// under the cooperative GIL.
2757+
v.hashKind = (inv.s == "Lock::Async") ? "Lock::Async" : "Lock";
27552758
if (parallelMode_) v.ext = std::make_shared<LockState>();
27562759
}
27572760
return v;
@@ -3692,7 +3695,7 @@ Value Interpreter::methodCall(Value inv, const std::string& m, ValueList args, c
36923695
return h;
36933696
}
36943697
}
3695-
if (inv.t == VT::Hash && inv.hashKind == "Lock") {
3698+
if (inv.t == VT::Hash && (inv.hashKind == "Lock" || inv.hashKind == "Lock::Async")) {
36963699
auto st = inv.ext ? std::static_pointer_cast<LockState>(inv.ext) : nullptr;
36973700
if (m == "protect" || m == "protect-or-queue-on-recursion") {
36983701
if (args.empty() || args[0].t != VT::Code) return args.empty() ? Value::any() : args[0];
@@ -4587,24 +4590,29 @@ Value Interpreter::methodCall(Value inv, const std::string& m, ValueList args, c
45874590
for (ClassInfo* c = ci.get(); c; c = c->parent.get()) chain.push_back(c);
45884591
for (auto it = chain.rbegin(); it != chain.rend(); ++it)
45894592
for (auto& at : (*it)->attrs) {
4590-
Value dv = at.hasDefVal ? at.defVal
4591-
: at.def ? eval(const_cast<Expr*>(at.def))
4592-
: (at.sigil == '@' ? Value::array()
4593-
: at.sigil == '%' ? Value::makeHash() : Value::any());
4594-
// native-typed scalars default to their zero, not Any:
4595-
// `has atomicint $.n` starts at 0 so `$!n⚛++` yields 0, 1, …
4596-
if (!at.hasDefVal && !at.def && at.sigil == '$' && !at.type.empty()) {
4593+
// The value the slot holds when it has no explicit default.
4594+
// A native-typed scalar takes its zero (`has atomicint $.n`
4595+
// starts at 0); a named type takes its TYPE OBJECT (not Any),
4596+
// so a `.= new` default reads that type object as its invocant
4597+
// (`has T $.x .= new` == `$!x = T.new`) — Rakudo semantics.
4598+
Value seed = at.sigil == '@' ? Value::array()
4599+
: at.sigil == '%' ? Value::makeHash() : Value::any();
4600+
if (at.sigil == '$' && !at.type.empty()) {
45974601
if (at.type == "atomicint" || at.type == "byte" ||
45984602
at.type.rfind("int", 0) == 0 || at.type.rfind("uint", 0) == 0)
4599-
dv = Value::integer(0);
4600-
else if (at.type.rfind("num", 0) == 0) dv = Value::number(0);
4601-
else if (at.type == "str") dv = Value::str("");
4602-
// a named type defaults to its TYPE OBJECT, not Any, so
4603-
// `has T $.x; … $!x .= new` works (T.new) — Rakudo semantics.
4604-
else if (std::isupper((unsigned char)at.type[0])) dv = Value::typeObj(at.type);
4603+
seed = Value::integer(0);
4604+
else if (at.type.rfind("num", 0) == 0) seed = Value::number(0);
4605+
else if (at.type == "str") seed = Value::str("");
4606+
else if (std::isupper((unsigned char)at.type[0])) seed = Value::typeObj(at.type);
46054607
}
46064608
if (!at.containerIs.empty() && at.sigil == '%')
4607-
dv = makeBaggy({}, at.containerIs); // has %.a is Set — empty Setty
4609+
seed = makeBaggy({}, at.containerIs); // has %.a is Set — empty Setty
4610+
// Pre-seed the slot so a self-referential default (`.= new`,
4611+
// or one reading $!this-attr) sees the seed, not an unset Any.
4612+
od->attrs[at.name] = seed;
4613+
Value dv = at.hasDefVal ? at.defVal
4614+
: at.def ? eval(const_cast<Expr*>(at.def))
4615+
: seed;
46084616
od->attrs[at.name] = dv;
46094617
}
46104618
// the default constructor binds nameds to declared PUBLIC attributes

src/Interpreter.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11960,6 +11960,17 @@ Value Interpreter::evalCall(Call* c) {
1196011960
// type-object coercion call: Any(x) / Mu(x) / Cool(x) is the value itself
1196111961
if ((c->name == "Any" || c->name == "Mu" || c->name == "Cool") && c->args.size() == 1)
1196211962
return eval(c->args[0].get());
11963+
// General type-coercion call `T(x)`: a known type used as a routine coerces its
11964+
// sole argument through the argument's `.T` method (Raku's coercion protocol) —
11965+
// e.g. Promise(supply {…}) == $supply.Promise, Supply($chan) == $chan.Supply.
11966+
// Only reached after the specialized coercers above, so it just upgrades former
11967+
// "Undefined routine" errors into real coercions (or a clearer "No such method").
11968+
if (!args.empty() && isKnownTypeName(c->name)) {
11969+
Value a0 = args[0];
11970+
if (a0.t == VT::Object && a0.obj && a0.obj->cls && a0.obj->cls->doesRole(c->name))
11971+
return a0; // already a T — identity
11972+
return methodCall(a0, c->name, ValueList{});
11973+
}
1196311974
throw RakuError{Value::str("Undefined routine &" + c->name),
1196411975
"Undefined routine '" + c->name + "'"};
1196511976
}

src/Parser.cpp

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4646,10 +4646,27 @@ StmtPtr Parser::parseClass(bool isRole, bool isGrammar, bool isPackage, bool isU
46464646
if (isKind(Tok::Ident) || isKind(Tok::Var)) advance();
46474647
if (isKind(Tok::LParen)) { int d = 0; do { if (isKind(Tok::LParen)) d++; else if (isKind(Tok::RParen)) d--; advance(); } while (d > 0 && !isKind(Tok::End)); }
46484648
}
4649-
if (matchOp("=") || matchOp(".=")) {
4649+
bool dotEq = false, hasDefault = true;
4650+
if (matchOp("=")) { }
4651+
else if (matchOp(".=")) dotEq = true;
4652+
else if (isOp(".") && peek().kind == Tok::Op && peek().text == "=") { advance(); advance(); dotEq = true; }
4653+
else hasDefault = false;
4654+
if (hasDefault) {
46504655
size_t defStart = pos_;
4651-
a.def = parseExpr(BP_ASSIGN);
4652-
checkVirtualCallInDefault(defStart);
4656+
if (dotEq) {
4657+
// `has T $x .= meth(args)` desugars to a default of
4658+
// `$!x.meth(args)` — the attr is seeded to T's type object at
4659+
// construction, and .meth (usually .new) coerces it. Build the
4660+
// method call so it actually runs (was dropped before).
4661+
auto mc = std::make_unique<MethodCall>();
4662+
mc->inv = std::make_unique<VarExpr>("$!" + a.name);
4663+
if (isKind(Tok::Ident) || isKind(Tok::Var)) mc->method = advance().text;
4664+
if (isKind(Tok::LParen) && !cur().spaceBefore) { advance(); mc->args = parseCallArgs(); }
4665+
a.def = std::move(mc);
4666+
} else {
4667+
a.def = parseExpr(BP_ASSIGN);
4668+
checkVirtualCallInDefault(defStart);
4669+
}
46534670
}
46544671
// a newline may end the declaration (`has $.cl = { … }` with no
46554672
// ';'): don't skip INTO the next class-body statement hunting one
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Regression: `has T $!x .= new` attribute initializers were dropped by the parser
2+
# (only `= EXPR` defaults were captured), so `$!x` stayed the bare type object.
3+
# `.= new` desugars to `$!x = $!x.new`, with `$!x` seeded to T's type object at
4+
# construction. Cro::HTTP::Client's `has Lock::Async $!lock .= new` needs this.
5+
# Contract: exit 0 + last line PASS.
6+
my @fail;
7+
8+
# user class, private attr
9+
class W { has $.n = 0; method bump { $!n++ } }
10+
class CP { has W $!w .= new; method go { $!w.bump; $!w.bump; $!w.n } }
11+
@fail.push('user-priv') unless CP.new.go == 2;
12+
@fail.push('user-priv-defined') unless (class :: { has W $!w .= new; method d { $!w.defined } }).new.d;
13+
14+
# public attr
15+
class CPub { has W $.w .= new; }
16+
@fail.push('user-pub') unless CPub.new.w.defined && CPub.new.w ~~ W;
17+
18+
# with constructor args
19+
class CArgs { has W $!w .= new(n => 5); method n { $!w.n } }
20+
@fail.push('with-args') unless CArgs.new.n == 5;
21+
22+
# built-in types: Lock::Async (keeps its own identity + shares Lock methods)
23+
class CL { has Lock::Async $!l .= new; method go { $!l.protect({ 42 }) } }
24+
@fail.push('lock-async') unless CL.new.go == 42;
25+
@fail.push('lock-async-name') unless Lock::Async.new.^name eq 'Lock::Async';
26+
27+
# Promise as a typed attr
28+
class CPr { has Promise $!p .= new; method d { $!p.defined } }
29+
@fail.push('promise') unless CPr.new.d;
30+
31+
# plain `= EXPR` defaults must be unaffected
32+
class CD { has $.x = 5; has Int $.y; has @.a = (1, 2, 3); }
33+
@fail.push('plain-default') unless CD.new.x == 5;
34+
@fail.push('typed-no-default') unless CD.new.y.^name eq 'Int' && !CD.new.y.defined;
35+
@fail.push('array-default') unless CD.new.a eqv [1, 2, 3];
36+
37+
if @fail { note "FAILED: @fail[]"; say 'FAIL' } else { say 'PASS' }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Regression: general `T(x)` type-coercion calls — a known type used as a routine
2+
# coerces its argument through the argument's `.T` method (Raku's coercion
3+
# protocol), instead of failing with "Undefined routine 'T'" for types outside
4+
# the Int/Str/... fast-path set.
5+
# Contract: exit 0 + last line PASS.
6+
my @fail;
7+
8+
# the specialized coercers still route correctly
9+
@fail.push('int-coerce') unless Int('42') == 42;
10+
@fail.push('str-coerce') unless Str(42) eq '42';
11+
@fail.push('num-coerce') unless Num('1.5') == 1.5e0;
12+
13+
# Supply($channel) coercion == $channel.Supply (a type outside the fast set)
14+
my $c = Channel.new; $c.send(7); $c.send(8); $c.close;
15+
@fail.push('supply-coerce') unless Supply($c).list eqv (7, 8);
16+
17+
# a user coercion method: `MyT($x)` == `$x.MyT`
18+
class Celsius { has $.c; method Str { "{$!c}C" } }
19+
@fail.push('str-of-obj') unless Str(Celsius.new(c => 20)) eq '20C';
20+
21+
if @fail { note "FAILED: @fail[]"; say 'FAIL' } else { say 'PASS' }

0 commit comments

Comments
 (0)