制約パーティショニング

めも。

クエリでプレースホルダ名をtypoされた時のエラー報告について。

/**
 * @when
 *   ret.result = 0
 */
public void checkReturnZero(CtReturn<?> returnStmt) {
  ...
}

上のクエリは、引数がreturnStmtに対して、クエリ内でretというプレースホルダを用意してそこに制約かけてる。
いくら頑張ってもretに制約かかるだけなので、returnStmtは「CtReturn型」という制約だけで動くのでよろしくない。retの計算は無意味に。

これはおそらく意図しない動作なので、ここでエラーを出す方法についてアイデア。制約関係でざくっとパーティショニングして、明らかに無意味なプレースホルダを探したい。

上のクエリをコンパイルすると、下のようなアセンブリ言語っぽい内部表現を生成。

IrQuery:
  // returnStmtはHack対象集合内のいずれか
  returnStmt in #universe#0
  // returnStmt はCtReturn型
  returnStmt:[RETURN]
  // retはHack対象集合内のいずれか
  ret in #universe#1
  // ret.result を ##2 に束縛
  ##2 = ret.result
  // ##2 は定数 0 に等しい
  ##2 equals 0

あるプレースホルダPが属するパーティションを、partition(P)で表現する。疑似コードとしてはこんな感じ。

partition(placeholder P) returns result:set-of-placeholder
  result := {P}
  for c in constraint(P)
    for r in placeholder(c)
      result := result ∪ r

constraint(set-of-placeholder P) returns result:set-of-constraint
  (プレースホルダPが出現するすべての制約式)

placeholder(set-of-constraint C) returns result:set-of-placeholder
  (制約式Cに出現するすべてのプレースホルダ)

そのうえで、メソッドパラメータと同名のプレースホルダの集合をMSとおいて、次の計算を行う(∪{p in MS} partition(p))。

parameter-relations(set-of-placeholder MS) returns result:set-of-placeholder
  result := {}
  for p in MS
    result := result ∪ partition(p)

最後に、元のクエリで出現した全てのプレースホルダの集合をOPSとおいて、OPSからparameter-relations(MS)の差集合を計算。要素が残っていれば、そのプレースホルダはパラメータに「無関係」であるといえる。はず。

先程の例で探すとこんな感じ。

  • partition(returnStmt) = {returnStmt, #universe#0}
  • partition(ret) = {ret, #universe#1, ##2}
  • parameter-relations(parameters in checkReturnZero) = {returnStmt, #universe#0}
  • {ret, returnStmt} - {returnStmt, #universe#0} = {ret}

この場合、retはreturnStmtに対して何の制約も課さないのでたぶんtypoだよね。という感じで。

ただ、これだけだと少々ぬるい。

ぬるい理由は↓。

/** retはmの中で定義され、返す値は定数1と同値。
 * @when
 *     ret in m.body
 *     ret.result = 1
 */
public void checkReturnZero2(CtMethod<?> m, CtReturn<?> r) {
  ...
}

コンパイルする。

IrQuery:
  m in #universe#0
  m : {@link CtMethod<?>}
  r in #universe#1
  r : {@link CtReturn<?>}
  ##2 = m.body
  ret in ##2
  ##3 = ret.result
  ##3 := 1
  • partition(m) = {m, ret, #universe#0, ##2, ##3}
  • partition(r) = {r, #universe#1}
  • parameter-relations(parameters in checkReturnZero2) = {m, ret, r, ...}
  • {m, r, ret} - {m, ret, r, ...} = {}

これは、「ret in m.body」を経由して ret が partition(m) に入り込んでいるため、最終的には ret が無駄な制約でないと判断されている。確かに、まったくrに影響が出ないわけでもない。

こういうときはちゃんとプレースホルダの宣言をした方がいいのか、悩み中。