Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/Compile/Mir/Ownership.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ private function ownCode(Type $t): int
return $t->kind === Type::KIND_UNION ? self::OBJ : self::VEC;
}

/** `+ - *` over cells run {@see Passes\EmitLlvmExpr::emitTaggedArith}, which mints a
* fresh value on every path: a numeric cell for all three, a union array for `+`. */
public static function cellArithMints(Node $n): bool
{
$k = $n->kind;
if ($k === Node::KIND_ADD) { return $n->type->kind === Type::KIND_CELL; }
return ($k === Node::KIND_SUB || $k === Node::KIND_MUL) && $n->type->isNumericCell();
}

public static function isClosureClass(string $cls): bool
{
return $cls === 'Closure' || \str_starts_with($cls, '__closure_');
Expand Down Expand Up @@ -349,6 +358,8 @@ private function storedOwned(Node $value): bool
// A string / cell bitwise op mints its result like a concat, on the
// heap whatever the allocKind says ({@see BitOp::mintsFresh}).
if (BitOp::mintsFresh($value)) { return true; }
// …and so does a cell `+ - *` ({@see cellArithMints}, the temp half's twin).
if (self::cellArithMints($value)) { return true; }
// `(string)$int` / `(string)$float` ALLOCATE — __mir_int_to_str and
// __mir_float_to_str hand back a fresh rc=1 buffer exactly as a string
// builtin does. This was the one producer nobody owned: the local took
Expand Down Expand Up @@ -884,8 +895,10 @@ public function tempCellOwned(Node $n, bool $lastCallWasBuiltin): bool
// `+ - *` over a numeric cell run {@see Passes\EmitLlvmExpr::emitTaggedArith}:
// the helper boxes a NEW cell on every path, a counted heap box past the
// inline int form.
if (($k === Node::KIND_ADD || $k === Node::KIND_SUB || $k === Node::KIND_MUL)
&& $n->type->isNumericCell()) { return true; }
// A cell `+` of two arrays is the union (`__manticore_tagged_add` boxes a
// fresh `__mir_array_union`), so any cell-typed `+` mints too; read as a
// borrow, `$x = ($a[$k] ?? []) + ($b[$k] ?? []); return $x;` leaked the union.
if (self::cellArithMints($n)) { return true; }
if (BitOp::mintsFresh($n)) { return true; }
if ($k !== Node::KIND_CALL) { return false; }
$fn = $n->function;
Expand Down Expand Up @@ -1458,8 +1471,7 @@ public function returnBorrowsCell(Node $v, bool $moved, bool $erasedArray): bool
if ($this->returnCondNormalized($v, $erasedArray)) { return false; }
if ($this->isUserOffsetGetRead($v)) { return false; }
// A numeric op's cell is minted by its helper ({@see tempCellOwned}).
if (($k === Node::KIND_ADD || $k === Node::KIND_SUB || $k === Node::KIND_MUL)
&& $v->type->isNumericCell()) { return false; }
if (self::cellArithMints($v)) { return false; }
return true;
}

Expand Down Expand Up @@ -1921,6 +1933,15 @@ public static function castArrayMints(Cast $c): bool
{
if ($c->target !== 'array') { return false; }
$ok = $c->operand->type->kind;
// An ARRAY operand passes through, and with it its count: a fresh +1 producer's
// result stays owned. Read as a borrow, `fn ($n) => (array) R::resolve($n)` was
// retained again on return and the resolved array leaked (Laravel's
// Router::resolveMiddleware, once per request).
if ($ok === Type::KIND_ARRAY) {
$k = $c->operand->kind;
return $k === Node::KIND_METHOD_CALL || $k === Node::KIND_STATIC_CALL
|| $k === Node::KIND_INVOKE || $k === Node::KIND_ARRAY_LIT;
}
// An OBJECT mints too: its declared slots are built into a fresh assoc
// (and a bag-only class's bag is retained). Read as a borrow, the
// result was retained again on return and never released in a local —
Expand Down
12 changes: 9 additions & 3 deletions src/Compile/Mir/Passes/EmitLlvm.php
Original file line number Diff line number Diff line change
Expand Up @@ -3032,12 +3032,13 @@ private function isOwnElemFlavor(string $flavor): bool

// Generator frame layout:
// resume_fn@0, state@8, current@16, key@24, nextkey@32,
// sent@40, retval@48, locals@56+
// sent@40, retval@48, env@56, locals@64+
// state: 0 = not started, k = suspended at yield k, -1 = finished (or
// running: the resume entry stores it, a yield overwrites it);
// -2 - s = being destroyed from state s (`__mir_gen_destroy`).
// The frame owns current / key / sent / retval, each a tagged cell.
private const GEN_HEADER = 56;
// The frame owns current / key / sent / retval / env, each a tagged cell;
// env is a closure generator's own closure (its captures are read from it).
private const GEN_HEADER = 64;

/** Count `yield` nodes in a generator body (state-machine arity). */
private function countYields(Node $n): int
Expand Down Expand Up @@ -4378,6 +4379,11 @@ private function retainCellPayload(Node $value): string
&& $k !== Type::KIND_CLOSURE && !$borrowedCellArray) {
return '';
}
// `(array)` over a minting operand ({@see \Compile\Mir\Ownership::castArrayMints})
// hands its +1 on: retained again, `$out[$k] = (array) resolve($n)` leaked it.
if ($value instanceof \Compile\Mir\Cast && \Compile\Mir\Ownership::castArrayMints($value)) {
return '';
}
$saveV = $this->lastValue;
$saveT = $this->lastValueType;
$out = $this->coerceToI64();
Expand Down
5 changes: 5 additions & 0 deletions src/Compile/Mir/Passes/EmitLlvmBuiltins.php
Original file line number Diff line number Diff line change
Expand Up @@ -9552,6 +9552,10 @@ private function biIsAStringSubject(array $args, bool $strict, string $target):
$this->rt->needsStrcmp = true;
$out = $this->emitNode($args[0]);
$out .= $this->coerceToI64();
// A minted subject (`strval($x)` — what an erased subject is unboxed by,
// {@see biIsA} — or `strtolower(...)`) is consumed by the compares and
// dropped here: Laravel's Container::build leaked one class name per make().
$fresh = $this->isFreshStringTemp($args[0]) ? $this->lastValue : '';
$strP = $this->ssa->allocReg();
$out .= ' ' . $strP . ' = inttoptr i64 ' . $this->lastValue . " to ptr\n";
$out .= $this->emitNode($args[1]);
Expand All @@ -9569,6 +9573,7 @@ private function biIsAStringSubject(array $args, bool $strict, string $target):
$out .= ' ' . $or . ' = or i1 ' . $acc . ', ' . $eq . "\n";
$acc = $or;
}
if ($fresh !== '') { $out .= $this->rcReleaseReg($fresh, 'str'); }
if ($acc === '') { return $this->biConstBool($out, false); }
$r = $this->ssa->allocReg();
$out .= ' ' . $r . ' = zext i1 ' . $acc . " to i64\n";
Expand Down
92 changes: 84 additions & 8 deletions src/Compile/Mir/Passes/EmitLlvmCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -1867,11 +1867,13 @@ private function emitInvoke(Invoke_ $n): string
// `($this->hooks[$k])()` calls an env the ELEMENT owns, and the body
// may take it off its slot (a hook that unregisters itself), which now
// gives the count back. Hold one for the call, as php does.
$calleeCallOwned = $this->calleeIsOwnedCall($iv->callee);
if (!$this->invokePinsCallee($iv, $iv->callee)) {
// A closure LITERAL callee (`(function () use ($x) {…})()`) is a fresh +1
// nobody else binds: give it back once the call returns.
// A generator closure is the exception: the Generator keeps borrowing the env.
if ($iv->callee->kind === Node::KIND_CLOSURE && !$n->type->isGenerator()) {
// nobody else binds, and so is a conditional's (`($cb ?? $default)()` retains
// the arm it picked): give it back once the call returns. A generator closure
// holds its own env in the frame ({@see EmitLlvmGenerator}), so it is no exception.
if ($iv->callee->kind === Node::KIND_CLOSURE || $this->condOwnsResult($iv->callee) || $calleeCallOwned) {
$this->rt->needsClosureRc = true;
$env = $this->lastValue;
$out .= $this->emitClosureStructInvoke($n, $env);
Expand Down Expand Up @@ -1966,13 +1968,36 @@ private function emitErasedExplicitInvoke(Node $n, \Compile\Mir\MethodCall_ $mc)
}


/**
* A callee that is a call's result is +1 under the return convention — a method, a
* static method, an invoke, or a body this module defines (not a builtin, not by
* reference) — and nothing else binds it: `call_user_func($this->getRouteResolver())`
* leaked Laravel's route resolver closure, with the Route it captured, per request.
* Read right after the callee was emitted ({@see $lastCallWasBuiltin}).
*/
private function calleeIsOwnedCall(Node $c): bool
{
$k = $c->kind;
if ($k === Node::KIND_METHOD_CALL || $k === Node::KIND_STATIC_CALL || $k === Node::KIND_INVOKE) { return true; }
if ($k !== Node::KIND_CALL || $this->lastCallWasBuiltin) { return false; }
$fn = $c->function;
return isset($this->sigs->paramTypes[$fn]) && !($this->sigs->returnsByRef[$fn] ?? false);
}

private function emitErasedInvoke(Invoke_ $n): string
{
$shared = $this->erasedInvokeShareable($n);
$out = $this->emitNode($n->callee);
// The `[$c, $m]` of `$c::$m()` has no other owner. Only an array: a generator keeps borrowing
// the closure that made it, and an erased result does not say whether it is one.
$calleeFlavor = $n->callee->type->isArray() ? $this->freshRcArgFlavor($n->callee) : '';
// The `[$c, $m]` of `$c::$m()` has no other owner, nor has a conditional's pick
// (`call_user_func($this->cb ?? $default, …)` retained `$default` and leaked it
// with everything it captured, per Laravel request). A generator closure holds
// its own env ({@see EmitLlvmGenerator}), so releasing after the call is safe.
$calleeFlavor = $n->callee->type->isArray() ? $this->freshRcArgFlavor($n->callee)
: ($this->condOwnsResult($n->callee) ? $this->condFlavor($n->callee->type)
: ($n->callee->kind === Node::KIND_CLOSURE ? 'closure'
: ($n->callee->type->kind === Type::KIND_CELL && $this->calleeIsOwnedCall($n->callee) ? 'cell'
: (($n->callee->type->kind === Type::KIND_CLOSURE || $n->callee->type->kind === Type::KIND_UNKNOWN)
&& $this->calleeIsOwnedCall($n->callee) ? 'callable' : ''))));
$out .= $this->coerceToI64();
$raw = $this->lastValue;
/** @var array<string, string> $owned */
Expand All @@ -1987,7 +2012,7 @@ private function emitErasedInvoke(Invoke_ $n): string
if ($calleeFlavor !== '' && \str_starts_with($raw, '%')) {
$resReg = $this->lastValue;
$resTy = $this->lastValueType;
$out .= $this->rcReleaseReg($raw, $calleeFlavor);
$out .= $calleeFlavor === 'callable' ? $this->callableWordDropIr($raw) : $this->rcReleaseReg($raw, $calleeFlavor);
$this->lastValue = $resReg;
$this->lastValueType = $resTy;
}
Expand Down Expand Up @@ -2291,6 +2316,48 @@ private function erasedInvokeHelper(string $modes, int $srcArgc): string
return $sym;
}

/**
* Drop a `callable` word whose representation is only known at run time: a
* tagged cell, a raw closure env, a raw callable array or a raw object. A raw
* pointer is tagged by its allocator header (array magic at -8 → array cell,
* anything else → object cell, whose drop tells an object from a closure env)
* and dropped as that cell. `call_user_func($this->getRouteResolver())` with
* `@return \Closure` leaked Laravel's route resolver and its Route per request.
*/
private function callableWordDropIr(string $w): string
{
$this->rt->needsTagged = true;
$tagL = $this->ssa->allocLabel('cwd.tag');
$rawL = $this->ssa->allocLabel('cwd.raw');
$ptrL = $this->ssa->allocLabel('cwd.ptr');
$endL = $this->ssa->allocLabel('cwd.end');
$istag = $this->ssa->allocReg();
$out = ' ' . $istag . ' = icmp ugt i64 ' . $w . ', ' . (string)\Compile\MemoryAbi::CELL_TAGGED_MIN . "\n";
$out .= ' br i1 ' . $istag . ', label %' . $tagL . ', label %' . $rawL . "\n";
$out .= $tagL . ":\n call void @__mir_cell_drop(i64 " . $w . ")\n br label %" . $endL . "\n";
$big = $this->ssa->allocReg();
$out .= $rawL . ":\n " . $big . ' = icmp ugt i64 ' . $w . ', ' . (string)\Compile\MemoryAbi::CELL_OBJ_MIN_PAYLOAD . "\n";
$out .= ' br i1 ' . $big . ', label %' . $ptrL . ', label %' . $endL . "\n";
$p = $this->ssa->allocReg();
$hp = $this->ssa->allocReg();
$h = $this->ssa->allocReg();
$hm = $this->ssa->allocReg();
$isArr = $this->ssa->allocReg();
$tag = $this->ssa->allocReg();
$cell = $this->ssa->allocReg();
$out .= $ptrL . ":\n " . $p . ' = inttoptr i64 ' . $w . " to ptr\n";
$out .= ' ' . $hp . ' = getelementptr inbounds i8, ptr ' . $p . ", i64 -8\n";
$out .= ' ' . $h . ' = load i64, ptr ' . $hp . "\n";
// Array buffers carry RC_TAG_MAGIC + 1..3 at -8 (vec / hashed / arena), an object + 0.
$out .= ' ' . $hm . ' = sub i64 ' . $h . ', ' . (string)(\Compile\MemoryAbi::RC_TAG_MAGIC + 1) . "\n";
$out .= ' ' . $isArr . ' = icmp ult i64 ' . $hm . ", 3\n";
$out .= ' ' . $tag . ' = select i1 ' . $isArr . ', i64 ' . (string)\Compile\MemoryAbi::CELL_ARRAY_TAG_BITS
. ', i64 ' . (string)\Compile\MemoryAbi::CELL_OBJ . "\n";
$out .= ' ' . $cell . ' = or i64 ' . $w . ', ' . $tag . "\n";
$out .= ' call void @__mir_cell_drop(i64 ' . $cell . ")\n br label %" . $endL . "\n";
return $out . $endL . ":\n";
}

/**
* Evaluate every argument that RUNS CODE (a call, an invoke, a `new`, a
* concat, an array literal) once, after the callee, into a slot, and answer
Expand Down Expand Up @@ -5041,7 +5108,16 @@ private function emitDiscardedCallRelease(Node $s): string
$out = $this->coerceToI64();
return $out . $this->rcReleaseReg($this->lastValue, $pf);
}
if ($this->lastCallWasBuiltin) { return ''; }
// A MINTING builtin (`json_decode('[]');`, `str_repeat(…);`) hands its
// result at +1 like a body does; nothing binds it, so drop it here —
// Laravel's JsonResponse::setData leaked one array per request. A
// borrowing builtin classifies Borrow and is left alone.
if ($this->lastCallWasBuiltin) {
if ($this->own->classifyTemp($s, true) <= 0) { return ''; }
$bf = $this->isClosureValueType($s->type) ? 'closure' : $this->discardReleaseFlavor($s->type);
if ($bf === '') { return ''; }
return $this->coerceToI64() . $this->rcReleaseReg($this->lastValue, $bf);
}
if (!isset($this->sigs->paramTypes[$fname])) { return ''; }
// Value context derefs the address and takes a +1 copy
// ({@see byRefValueCopyRetainIr}). Only an array copy is counted; a raw
Expand Down
8 changes: 8 additions & 0 deletions src/Compile/Mir/Passes/EmitLlvmControl.php
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,14 @@ private function armIsFresh(Node $arm, string $flavor): bool
|| $k === Node::KIND_INVOKE || \Compile\Mir\BitOp::mintsFresh($arm)) {
return true;
}
// An array `+` is `__mir_array_union`'s fresh +1 buffer ({@see
// \Compile\Mir\Ownership} owns it like a literal); retained again, Laravel's
// `RouteCollection::get` leaked the union on every request.
if ($k === Node::KIND_ADD && $arm->type->kind === Type::KIND_ARRAY
&& $arm->allocKind === \Compile\Mir\AllocationKind::RC_HEAP) {
return true;
}
if (\Compile\Mir\Ownership::cellArithMints($arm)) { return true; }
if ($k === Node::KIND_CALL) {
$fn = $arm->function;
if ($this->sigs->returnsByRef[$fn] ?? false) { return false; }
Expand Down
10 changes: 10 additions & 0 deletions src/Compile/Mir/Passes/EmitLlvmExceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -777,8 +777,18 @@ private function ehCallRest(string $l): string
/** Runtime helpers that never run user code and never throw. */
private function ehNounwind(string $callee): bool
{
// Each invoke costs a block and an unwind edge per call site: in the
// Laravel build the backtrace ring alone was 648K of them.
return $callee === '@__mc_eh_catch'
|| $callee === '@__mir_elem_untag'
|| $callee === '@__mir_bt_push'
|| $callee === '@__mir_bt_push_rel'
|| $callee === '@__mir_bt_pop'
|| $callee === '@__manticore_deref'
|| $callee === '@__manticore_tag'
|| $callee === '@__manticore_cell_int_bad'
|| $callee === '@__mir_obj_handle_alloc'
|| \str_starts_with($callee, '@__mc_dynf_')
|| \str_starts_with($callee, '@__mir_arena_')
|| \str_starts_with($callee, '@__mir_rc_retain')
|| \str_starts_with($callee, '@__manticore_box_');
Expand Down
Loading
Loading