067: showlist2.hs がランタイムに abend

↑up

概要

055 の調査のために、再現用の小さなテストプロを作成。

showlist2.hs:

$ cat testcases/showlist2.hs
main = do
  putStrLn $ showList [1] ""
    where
      showList xs = (++) (showlist' xs)
        where showlist' [] = "[]"
              showlist' [x] =  "[" ++ show x ++ "]"
              showlist' (x:xs) = "[" ++ foldl (\s t -> s ++ "," ++ show t) (show x) xs ++ "]"

これをコンパイル、実行するとランタイムでエラーする。 なお、これに似た showlist.hs は大丈夫。

showlist.hs:

main = do
  putStrLn $ showList [1] ""
    where showlist' [] = "[]"
          showlist' [x] =  "[" ++ show x ++ "]"
          showlist' (x:xs) = "[" ++ foldl (\s t -> s ++ "," ++ show t) (show x) xs ++ "]"

          showList xs = (++) (showlist' xs)

また、showlist2.hs の showlist' に型注釈をつけたもの(showlist3.hs)も Ok.

エラーするケースでは、showlist' 内部における辞書の解決がおかしいようにみえる。

let 式がネストされているケースの多相の解決(DictPass による辞書渡し形式への変換) に難があるようだ。flesh な type value にすべきところをしていないとか、かも。

うまくいくケースとそうでない場合の core0, core を見比べてみよう。

調査ログ

2020-05-12 (Tue)

だめな場合の core を確認すると、DARG0 を渡すべきところで、${Prelude.Char Prelude.Show} を渡しているよう。型注釈をつけた showlist3.hs では、 ${Prelude.Integer Prelude.Show} がその場で生成されているので OK。

ただ、まだ例が大きすぎてみるのが大変だし、被疑個所が特定しづらい。 そこで、もっと小さくしていって、エラーが出る場合と出ない場合の境界をあきらかにしたい。

どうやら、これが最小再現ケースではないかな (t067.hs):

main = do
  putStrLn $ f 1 ""
    where
      f xs str = str ++ (show' xs)
        where show' x =  show x

show' はここまで単純でも再現する。でも、let が nest されていて、show' の型が決まらない(多相になる)のは必須だと思われる。

instancee (Show a) => Show [a] の実装において発覚した問題だが、リストは関係なかったよう。また、元の例ではパターンマッチコンパイル結果の case 式が頻出していて、 その複雑な場合に発生する問題かなと恐れたのですが、そうでもなかった。

なお、f はこれ以上単純化する、たとえば、f xs str = show' xs のようにすると、もう、再現しない。

t067.hs の問題の core は次の通り:

---- ddumpCore ----
(Main.main :: (Prelude.IO ())) =
    let
        (Main.l1.l0.f :: ([Prelude.Show t5] :=> (t5 -> ([Prelude.Char] -> [Prelude.Char])))) =
            \(Main.l1.l0.f.DARG0 :: Ä) ->
                \(_Main.l1.l0.f.U1 :: ([Prelude.Show t0] :=> t0)) (_Main.l1.l0.f.U2 :: ([Prelude.Show t0] :=> [Prelude.Char])) ->
                    let
                        (Main.l1.l0.l0.l0.show' :: ([Prelude.Show t3] :=> (t3 -> [Prelude.Char]))) =
                            \(Main.l1.l0.l0.l0.show'.DARG0 :: Ä) ->
                                \(_Main.l1.l0.l0.l0.show'.U1 :: ([Prelude.Show t1] :=> t1)) ->
                                    (((Prelude.show :: ([Prelude.Show t2] :=> (t2 -> [Prelude.Char]))) 
                                        ${Prelude.Char Prelude.Show}) 
                                      (_Main.l1.l0.l0.l0.show'.U1 :: ([Prelude.Show t1] :=> t1)))
                        
                        
                    in
                      (((Prelude.++ :: ([t3] -> ([t3] -> [t3]))) 
                          (_Main.l1.l0.f.U2 :: ([Prelude.Show t0] :=> [Prelude.Char]))) 
                        (((Main.l1.l0.l0.l0.show' :: ([Prelude.Show t4] :=> (t4 -> [Prelude.Char]))) 
                            (Main.l1.l0.f.DARG0 :: Å)) 
                          (_Main.l1.l0.f.U1 :: ([Prelude.Show t0] :=> t0))))
        
        
    in
      ((Prim.putStrLn :: ([Prelude.Char] -> (Prelude.IO ()))) $ 
        ((((Main.l1.l0.f :: ([Prelude.Show t7] :=> (t7 -> ([Prelude.Char] -> [Prelude.Char])))) 
              ${Prelude.Integer Prelude.Show}) 
            (((Prelude.fromInteger :: ([Prelude.Num t8] :=> (Prelude.Integer -> t8))) 
                ${Prelude.Integer Prelude.Num}) 
              (1 :: Prelude.Integer)))  ""))

Main.l1.l0.l0.l0.show' のなかで、show に渡されている辞書が、 なぜ唐突に ${Prelude.Char Prelude.Show } なのかということ。

show' の外にある、f を単純にすると再現しないことから、ひとつ上のレベルの変な情報を拾ってしまっている模様。(そういえば、CompositDict を思いつくまえに、変な改造をいれたかな…)

f を少し単純にした t067b.hs のコアは以下のとおり:

---- ddumpCore ----
(Main.main :: (Prelude.IO ())) =
    let
        (Main.l1.l0.f :: ([Prelude.Show t5] :=> (t5 -> (t6 -> [Prelude.Char])))) =
            \(Main.l1.l0.f.DARG0 :: Ä) ->
                \(_Main.l1.l0.f.U1 :: ([Prelude.Show t0] :=> t0)) (_Main.l1.l0.f.U2 :: ([Prelude.Show t0] :=> t1)) ->
                    let
                        (Main.l1.l0.l0.l0.show' :: ([Prelude.Show t4] :=> (t4 -> [Prelude.Char]))) =
                            \(Main.l1.l0.l0.l0.show'.DARG0 :: Ä) ->
                                \(_Main.l1.l0.l0.l0.show'.U1 :: ([Prelude.Show t2] :=> t2)) ->
                                    (((Prelude.show :: ([Prelude.Show t3] :=> (t3 -> [Prelude.Char]))) 
                                        (Main.l1.l0.l0.l0.show'.DARG0 :: Å)) 
                                      (_Main.l1.l0.l0.l0.show'.U1 :: ([Prelude.Show t2] :=> t2)))
                        
                        
                    in
                      (((Main.l1.l0.l0.l0.show' :: ([Prelude.Show t4] :=> (t4 -> [Prelude.Char]))) 
                          (Main.l1.l0.f.DARG0 :: Å)) 
                        (_Main.l1.l0.f.U1 :: ([Prelude.Show t0] :=> t0)))
        
        
    in
      ((Prim.putStrLn :: ([Prelude.Char] -> (Prelude.IO ()))) $ 
        ((((Main.l1.l0.f :: ([Prelude.Show t7] :=> (t7 -> (t8 -> [Prelude.Char])))) 
              ${Prelude.Integer Prelude.Show}) 
            (((Prelude.fromInteger :: ([Prelude.Num t9] :=> (Prelude.Integer -> t9))) 
                ${Prelude.Integer Prelude.Num}) 
              (1 :: Prelude.Integer)))  ""))

上手くいっている方は、_Main.l1.l0.f.U2 の型が違う(多相)。 だめな方は、これをなぜか拾っている??

おお、そうか。_Main.l1.l0.f.U1 と _Main.l1.l0.f.U2 の量化変数がともに t0 になっていて、 まざっているんだと思われる。

Core0 の時点でおかしいので、問題は DictPass ではなく、TrCore あたりにある。

---- ddumpCore ----
(Main.main :: (Prelude.IO ())) =
    let
        (Main.l1.l0.f :: ([Prelude.Show t5] :=> (t5 -> ([Prelude.Char] -> [Prelude.Char])))) =
            \(_Main.l1.l0.f.U1 :: ([Prelude.Show t0] :=> t0)) (_Main.l1.l0.f.U2 :: ([Prelude.Show t0] :=> [Prelude.Char])) ->

ちょっと怖いので、ブランチをわけて修正作業をしよう。あとで。

TrCore 修正

上記の件で、TrCore を修正。_Main.l1.l0.f.U2 の型から不要な Predicate は消えるようになったが、辞書渡しがおかしいのは変わらず。

$ git diff
diff --git a/compiler/src/TrCore.hs b/compiler/src/TrCore.hs
index 291a029..5abc145 100644
--- a/compiler/src/TrCore.hs
+++ b/compiler/src/TrCore.hs
@@ -6,7 +6,7 @@ import           PreDefined                 (ConstructorInfo, initialConsts)
 import           Symbol
 import           Types
 import           Typing                     (Assump (..), Pred (..), Qual (..),
-                                             Scheme (..), find, inst)
+                                             Scheme (..), find, inst, tv)
 import qualified Typing                     as Ty (Expr (..), Literal (..),
                                                    Pat (..))
 
@@ -120,8 +120,9 @@ transVdef (n, Pat.Lambda ns expr) = do
       vs = zipWith f ns ts
       qf = case qt of
         (qf' :=> _) -> qf'
-      f n' t' = TermVar n' (qf :=> t')
-      as' = [n' :>: Forall [] (qf :=> t') | (n', t') <- zip ns ts]
+      f n' t' = let qf' = filter (\pr -> head (tv pr) `elem` tv t') qf
+                in TermVar n' (qf' :=> t')
+      as' = [n' :>: Forall [] (qf' :=> t') | TermVar n' (qf' :=> t') <- vs]
   appendAs as'
   expr' <- transExpr expr
   appendBind (n, lam' vs expr')

このあたりの実装は、最後あたりに整理しなおさないといけなさそう。 あやふやな理解のまま書いた風なところが多い。

もしかして、DictPass では、Lambda の型を assump からとればいい (TrCore で登録してある)のに、TrCore の古いバージョンと同じやり方で生成しているのでは?

DictPass 調査(継続)

tcExpr e@(Var n (qv :=> t'))) qt にトレースを仕掛けてみると、 ここで問題の "show" 呼び出しを処理するときに与えられる qt が変。

(Var (TermVar "Prelude.show" ([IsIn "Prelude.Show" (TVar (Tyvar "t2" Star))] :=> TAp (TAp (TCon (Tycon "(->)" (Kfun Star (Kfun Star Star)))) (TVar (Tyvar "t2" Star))) (TAp (TCon (Tycon "Prelude.[]" (Kfun Star Star))) (TCon (Tycon "Prelude.Char" Star))))),[] :=> TAp (TAp (TCon (Tycon "(->)" (Kfun Star (Kfun Star Star)))) (TCon (Tycon "Prelude.Char" Star))) (TAp (TCon (Tycon "Prelude.[]" (Kfun Star Star))) (TCon (Tycon "Prelude.Char" Star))))

これでは、show の第一引数の型は Char なので、${Prelude.Char Prelude.Show} が生成される。

被疑箇所?

仮に、Let の tcExpr 中で、tcBind に state を引き継がないようにすると、 t067.hs は通るようになる(他がだめになるが)。

let の中と外でなにかコンフリクトしている模様。

tcExpr (Let bs e) qt = do
  st <- get
  let bs' = tcBind bs (tcCe st) (Just st)
  e' <- tcExpr e (normQTy qt)
  return $ Let bs' e'

tcBind にせっかく状態をわたしているのに、そこで更新された tcNum をあとに引き継いでなかった(複数の tcbind 間でも)のを修正。

それで改善するかと思ったが、そうはいかず。 でも、この修正は必要だったとは思われる。

2020-05-13 (Wed)

t067.hs 処理のどこで初めて t1 なり t2 が Char になってしまうか捉えるために、 check&stoper を仕掛ける。

-- checkStoper (for 067)                                                                                                                                                                     
checkStop :: TC ()
checkStop = do
  s <- getSubst
  let t1 = TVar (Tyvar "t1" Star)
      t2 = TVar (Tyvar "t2" Star)
      t1' = apply s t1
      t2' = apply s t2
  trace (show (t1,t1',t2,t2')) $ return ()
  when (t1' == tChar) $ error "checkStop t1' = tChar"
  when (t2' == tChar) $ error "checkStop t2' = tChar"

tcExpr Lam における unify' 直後の checkStop で停止した:

tcExpr e@(Lam vs ebody) (qs :=> t) = do
  trace ("tcExpr 0:" ++ show e ++ " (" ++ show qs ++ " :=> " ++ show t ++ ")") checkStop
  
  _ :=> t' <- getTy e
  unify' t' t

  trace (show t') $ return ()
  trace ("tcExpr 1:" ++ show e ++ " (" ++ show qs ++ " :=> " ++ show t ++ ")") checkStop

  qt <- getTy ebody
  s <- getSubst
  ebody' <- tcExpr ebody (normQTy (apply s qt))
  return $ Lam vs ebody'

つかんだ尻尾は以下:

TAp (TAp (TCon (Tycon "(->)" (Kfun Star (Kfun Star Star)))) (TVar (Tyvar "t1" Star))) (TAp (TCon (Tycon "Prelude.[]" (Kfun Star Star))) (TCon (Tycon "Prelude.Char" Star)))
tcExpr 1:
Lam [TermVar "_Main.l1.l0.l0.l0.show'.U1" ([IsIn "Prelude.Show" (TVar (Tyvar "t1" Star))] :=> TVar (Tyvar "t1" Star))]
    (App (Var (TermVar "Prelude.show" ([IsIn "Prelude.Show" (TVar (Tyvar "t2" Star))] :=> TAp (TAp (TCon (Tycon "(->)" (Kfun Star (Kfun Star Star)))) (TVar (Tyvar "t2" Star))) (TAp (TCon (\
Tycon "Prelude.[]" (Kfun Star Star))) (TCon (Tycon "Prelude.Char" Star)))))) (Var (TermVar "_Main.l1.l0.l0.l0.show'.U1" ([IsIn "Prelude.Show" (TVar (Tyvar "t1" Star))] :=> TVar (Tyvar "t1"\
 Star)))))
    ([IsIn "Prelude.Show" (TVar (Tyvar "t3" Star))] :=>
     TAp (TAp (TCon (Tycon "(->)" (Kfun Star (Kfun Star Star)))) (TVar (Tyvar "t3" Star))) (TAp (TCon (Tycon "Prelude.[]" (Kfun Star Star))) (TCon (Tycon "Prelude.Char" Star))))
bunnyc: checkStop t1' = tChar

ここでは、t1 -> [Char] と t3 -> [Char] を unify して、t1 -> Char に なってしまっている。

しらべると、この時点ですでに t3 -> Char となっていた。

なので、こんどは、t3 -> Char になった瞬間を捕まえなくてはいけない。

おっていくと、(++) :: [t3] -> [t3] -> [t3] で、これの引数に "" が与えられているので、 t3 -> Char と置換されるのは正しい。 問題は、show' に出現する show の型にも同じ型変数が用いられ、show :: (Show t3) => t3 -> [Char] となっているところ。

やっぱり、被疑個所は DictPass ではない。

原因判明

TrCore.appendBind に問題があった。

以下は修正後のコード:

appendBind :: (Id, Expr) -> TRC ()
appendBind (n, e) = do
  t <- typeLookup n
  st <- get -- this must be after the typeLookup above.                                                                                                                                      
  let Rec bs = trcBind st
      bs' = bs ++ [(TermVar n t, e)]
  put st{trcBind = Rec bs'}

typeLookup にて、Assump から識別子 n の型を検索し、かつ、 量化変数を具体化するのだが、そこで状態 st が更新される。

が、従来のコードは、st <- get を typelookup の前に行っていたため、 その後の put st{...} で、st の内部状態 trcNum が巻き戻ってしまい、 番号の重複につながっていた(t3 が二回生成されて衝突)。

これを直したことで、本件の問題は解決。

もともと ok だった、showlist.hs, showlist3.hs は廃止。