← All reports

[JSC] Extract CheckInBounds from StringAt and StringCodePointAt

The bounds check that got deleted because it had already passed.

Component: JavaScriptCore DFG/FTL JIT | 5d1761d

Source/JavaScriptCore/dfg/DFGSSALoweringPhase.cpp

- case StringCharCodeAt: {
+ case StringAt:
+ case StringCharCodeAt:
+ case StringCodePointAt: {
lowerStringBoundsCheck(m_graph.child(m_node, 0), m_graph.child(m_node, 1), m_graph.child(m_node, 2));
break;
}

Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

- LValue index = m_node->op() == StringAt ? m_out.select(m_out.lessThan(originalIndex, m_out.int32Zero), m_out.add(stringLength, originalIndex), originalIndex) : originalIndex;
+ bool boundsCheckLowered = m_node->op() == StringAt && m_node->arrayMode().isInBounds();
+ LValue index = m_node->op() == StringAt && !boundsCheckLowered ? m_out.select(m_out.lessThan(originalIndex, m_out.int32Zero), m_out.add(stringLength, originalIndex), originalIndex) : originalIndex;
 
LBasicBlock fastPath = m_out.newBlock();
- LBasicBlock slowPath = m_out.newBlock();
+ LBasicBlock slowPath = boundsCheckLowered ? nullptr : m_out.newBlock();
LBasicBlock continuation = m_out.newBlock();

JSC의 DFG와 FTL tier는 string index 접근이 범위를 벗어나지 않는다고 speculate합니다. 이 가정은 StringAt/StringCodePointAt 같은 노드 내부에 암묵적인 check 형태로 들어가 있습니다. index가 범위를 벗어나면 해당 노드는 exit, 즉 deoptimize를 수행해 하위 tier로 돌아가게 됩니다. 한편 abstract interpreter는 이 check가 반드시 통과했다는 사실을 근거로 삼습니다. 그래서 === undefined 같은 이후 비교 연산을 상수로 fold합니다. 이때 원래 노드의 값을 사용하는 쪽이 사라지고, dead-code elimination이 노드 자체를 제거합니다. 이번 commit은 SSA lowering 단계에서 bounds check를 독립적인 CheckInBounds 노드로 분리했습니다. 그 결과 StringAt, StringCharCodeAt, StringCodePointAt이 모두 lowerStringBoundsCheck()를 거치게 되었습니다. 또한 FTL lowering은 check가 이미 분리된 경우 자체 index clamping과 slow path를 건너뛰도록 수정되었습니다.

Before:
  string.codePointAt(index) === undefined
    ├─ AI folds comparison to `false` (assumes the implicit bounds check fired)
    └─ DCE: StringCodePointAt has no users ──► entire node removed
  Runtime: OOB/negative index no longer exits — folded `false` used anyway

After (SSALoweringPhase):
  CheckInBounds(index, length)   ← separate node, survives DCE
  StringAt / StringCodePointAt   ← still removable if unused
  Runtime: OOB/negative index still triggers CheckInBounds exit

패치 이전에는 string.codePointAt(index) === undefined가 out-of-bounds나 음수 index에 대해 safety exit를 타지 않고, 이미 fold된 낡은 결과를 반환할 수 있었습니다. 그래서 JIT으로 컴파일된 코드가 잘못된 bounds check 가정 위에서 계산을 수행했습니다.

다른 곳에서도 찾아볼 만한 bug class는, safety check — bounds, type, null — 를 독립적인 graph 노드로 표현하지 않고 자기 semantics 안에 묶어 둔 DFG 노드입니다. 이런 구조에서는 abstract interpreter의 folding이 노드의 값을 죽은 값으로 만드는 순간, check까지 조용히 함께 삭제됩니다. Narrow: DFGSSALoweringPhase.cpp와 FTLLowerDFGToB3.cpp에서 GetByVal/PutByVal을 비롯한 TypedArray 및 array 접근 노드들을 같은 관점으로 점검할 필요가 있습니다. 암묵적 check와 DCE가 맞물리는 동일한 위험이 있는지 확인하는 작업입니다. 특히 speculation의 결과가 interpreter가 fold할 수 있는 비교 연산에만 쓰이는 경우를 우선순위에 두면 좋습니다. Wider: 같은 형태는 array가 아닌 노드에 접혀 들어간 check에도 적용됩니다. interpreter가 상수로 증명할 수 있는 경우가 대상입니다. 노드의 효과는 guard인데 graph가 추적하는 것은 값뿐인 지점이 전부 여기에 해당합니다. 도달 범위는 lowerStringBoundsCheck 계열 helper가 다루지 않는 모든 노드 종류입니다. Widest: 어떤 optimizing compiler에서든, IR에서 일급 effect로 표현되지 않은 safety check는 liveness analysis 한 번이면 삭제될 수 있는 상태입니다. 지켜야 할 invariant는 guard가 노드의 속성이 아니라 노드 그 자체여야 한다는 점입니다. Code-review tell: lowering phase의 노드 case가 index clamp나 range 비교를 인라인으로 처리하면서, 대응되는 Check* 노드는 함께 emit하지 않는 경우입니다.