Spec-Zone.ru › Haskell 9

16. Известные ошибки и недостатки

16.1. Несоответствие языка стандартам Haskell и Glasgow Haskell

В этом разделе перечислены недостатки реализации Glasgow Haskell в отношении стандартов Haskell 98 и Haskell 2010. Сведения о сбоях, утечках памяти и других нежелательных явлениях см. также в разделе «Когда что-то идёт не так» (Что делать, если что-то пошло не так).

Ограничения перечислены здесь примерно в том же порядке, что и в отчёте Haskell.

16.1.1. Отклонения от Haskell 98 и Haskell 2010

GHC стремится вести себя (в основном) как компилятор Haskell 98 или Haskell 2010, если попросить его об этом с помощью флагов Haskell98 и Haskell2010. Известные отклонения от стандартов описаны ниже. Если не указано иное, отклонение относится к режимам Haskell 98 и Haskell 2010.

16.1.1.1. Лексический синтаксис

  • Некоторые лексические правила, касающиеся квалифицированных идентификаторов, в GHC немного отличаются от описанных в отчёте Haskell. Если у вас есть ⟨module⟩.⟨reservedop⟩, например M.\, GHC интерпретирует это как единый квалифицированный оператор, а не как две лексемы M и .\.
  • forall всегда является зарезервированным ключевым словом. Это противоречит отчёту Haskell, который позволяет называть переменные и переменные типа forall. Обратите внимание, что это не означает, что GHC всегда включает расширение ExplicitForAll. Даже если это расширение отключено, резервирование forall в качестве ключевого слова имеет значение. Например, GHC не сможет разобрать сигнатуру типа foo :: forall x.
  • Оператор (!), записанный в префиксной форме (с пробелом перед ним и без пробела после, как в f !x = ...), интерпретируется как шаблон bang, вопреки отчёту Haskell, предписывающему трактовать ! как оператор независимо от окружающих пробелов. Обратите внимание, что это не означает, что GHC всегда включает расширение BangPatterns. Если расширение отключено, GHC выдаст ошибку разбора для f !x и предложит включить расширение.
  • Непроверяемые шаблоны необходимо записывать в префиксной форме:

    f ~a ~b = ...    -- accepted by both GHC and the Haskell Report
    f ~ a ~ b = ...  -- accepted by the Haskell Report but not GHC
    

    В непрефиксной форме GHC трактует (~) как обычный инфиксный оператор.

    Точные правила см. в предложении GHC № 229.

  • Аннотации строгости в объявлениях данных необходимо записывать в префиксной форме:

    data T = MkT !Int   -- accepted by both GHC and the Haskell Report
    data T = MkT ! Int  -- accepted by the Haskell Report but not GHC
    

    Точные правила см. в предложении GHC № 229.

  • Шаблоны as не должны отделяться пробелами с обеих сторон:

    f p@(x, y, z) = ...    -- accepted by both GHC and the Haskell Report
    
    -- accepted by the Haskell Report but not GHC:
    f p @ (x, y, z) = ...
    f p @(x, y, z) = ...
    f p@ (x, y, z) = ...
    

    Если с обеих сторон есть пробелы, GHC трактует (@) как обычный инфиксный оператор.

    Если перед (@) есть пробел, а после него нет, оно трактуется как видимое применение типа.

    Точные правила см. в предложении GHC № 229.

  • Отчёт Haskell допускает любые десятичные цифры Unicode в десятичных литералах. Однако GHC принимает только цифры ASCII:

    ascDigit    →   0 | 1 | … | 9
    decimal     →   ascDigit {ascDigit}
    
  • GHC допускает более широкий набор символов в идентификаторах. Буквы Unicode из категории Other Letters считаются строчными буквами, поэтому с них могут начинаться идентификаторы переменных. К классу цифр относятся все числа Unicode, а не только десятичные цифры. В конце идентификаторов могут встречаться модифицирующие буквы и комбинируемые диакритические знаки:

    uniSmall    →   any Unicode Lowercase Letter or Other Letter
    uniDigit    →   any Unicode Decimal Number, Letter Number or Other Number
    
    uniIdchar   →   any Unicode Modifier Letter or Non-Spacing Mark
    idchar      →   small | large | digit | uniIdchar | '
    
    varid       →   small {idchar} ⟨reservedid⟩
    conid       →   large {idchar}
    
  • GHC допускает лишние скобки вокруг имени функции в части funlhs объявлений. Иными словами, GHC успешно разбирает объявление вида ((f)) x = <rhs> с любым количеством скобок вокруг f.

16.1.1.2. Контекстно-свободный синтаксис

  • В режиме Haskell 98 (но не в режиме Haskell 2010) GHC несколько менее строго применяет правило отступов в выражениях do. В частности, ограничение «вложенный контекст должен иметь отступ правее, чем охватывающий его контекст» ослаблено: вложенный контекст может находиться на том же уровне, что и охватывающий его, если вложенный контекст является выражением do.

    Например, следующий код, в котором контекст do вложен в контекст case, а оператор feed animal имеет такой же отступ, как и вариант case, принимается GHC:

    main = case animal of
             Wombat -> do
             feed animal
    

    Но этот код с обратным вложением не принимается:

    main = do
      case animal of
      Wombat -> feed animal
    

    Это поведение регулируется расширением NondecreasingIndentation.

NondecreasingIndentation
Начиная с:

7.2.1

Статус:

Включено в Haskell98

Разрешает располагать вложенные контексты на том же уровне отступа, что и охватывающий их контекст.

  • GHC не выполняет разрешение фиксированности операторов в выражениях на этапе разбора, как того требует Haskell 98 (но не Haskell 2010). Например, согласно отчёту Haskell 98, следующее выражение допустимо:

    let x = 42 in x == 42 == True
    

    и разбирается так:

    (let x = 42 in x == 42) == True
    

    поскольку, согласно отчёту, выражение let «простирается как можно дальше вправо». Поскольку оно не может продолжаться за второй знак равенства без ошибки разбора (== не является фиксированным), выражение let должно на нём завершиться. GHC просто захватывает всё выражение целиком и разбирает его следующим образом:

    (let x = 42 in x == 42 == True)
    

16.1.1.3. Выражения и шаблоны

По умолчанию GHC делает некоторые программы немного более определёнными, чем они должны быть. Например, рассмотрим

f :: [a] -> b -> b
f [] = error "urk"
f (x:xs) = \v -> v

main = print (f [] `seq` True)

Это должно вызвать error, но на самом деле выводит True. Причина: GHC выполняет η-расширение f до

f :: [a] -> b -> b
f []     v = error "urk"
f (x:xs) v = v

Для большинства программ это достаточно повышает эффективность, чтобы включить оптимизацию, хотя лишь в редких случаях она может иметь нежелательные последствия. Чтобы отключить эту оптимизацию, используйте -fpedantic-bottoms.

16.1.1.4. Шаблоны, допускающие отказ

Начиная с предложения MonadFail (MFP), блоки do-нотации, содержащие шаблон, допускающий отказ, должны иметь ограничение MonadFail.

Например

mayFail :: (MonadIO m) => m ()
mayFail = do
  (Just value) <- fetchData
  putStrLn value

Вы получите предупреждение

• Could not deduce (MonadFail m)
    arising from a do statement
    with the failable pattern ‘(Just x)’
  from the context: MonadIO m
    bound by the type signature for:
               mayFail :: forall (m :: * -> *). MonadIO m => m ()

И действительно, поскольку в классе Monad больше нет метода fail, нам нужно явно добавить (MonadFail m) к ограничениям функции.

16.1.1.5. Проверка типов рекурсивных групп связываний

Отчёт Haskell предписывает разбивать группу связываний (на верхнем уровне или в let либо where) на компоненты сильной связности, а затем проверять типы в порядке зависимостей (Отчёт Haskell, раздел 4.5.1). При проверке типов каждой группы все связыватели этой группы, имеющие явную сигнатуру типа, помещаются в окружение типов с указанным полиморфным типом, а все остальные остаются мономорфными до обобщения группы (Отчёт Haskell, раздел 4.5.2).

Следуя предложению Марка Джонса из его статьи «Типизация Haskell на Haskell», GHC реализует более общую схему. В GHC при анализе зависимостей игнорируются ссылки на переменные с явной сигнатурой типа. Благодаря этому уточнённому анализу зависимостей группы зависимостей становятся меньше, и проверку типов проходят больше связываний. Рассмотрим, например:

f :: Eq a => a -> Bool
f x = (x == x) || g True || g "Yes"

g y = (y <= y) || f True

В Haskell 98 этот код отвергается, но при использовании схемы Джонса определение g проверяется первым и отдельно от определения f, поскольку ссылка на f в правой части g игнорируется при анализе зависимостей. Затем тип g обобщается, и получается

g :: Ord a => a -> Bool

Теперь проверяются типы определения f, при этом в окружении типов находится указанный тип g.

Тот же уточнённый анализ зависимостей позволяет также задавать для сигнатур типов взаимно рекурсивных функций разные контексты, что запрещено в Haskell 98 (раздел 4.5.2, последнее предложение). GHC требует лишь, чтобы сигнатуры типов в уточнённой группе были идентичны; на практике это означает, что одинаковый контекст должны иметь только переменные, связанные одним и тем же связыванием по образцу. Например, следующий код допустим:

f :: Eq a => a -> Bool
f x = (x == x) || g True

g :: Ord a => a -> Bool
g y = (y <= y) || f True

16.1.1.6. Заголовки модулей по умолчанию с -main-is

Отчёт Haskell 2010 указывает в <https://www.haskell.org/onlinereport/haskell2010/haskellch5.html#x11-990005.1>, что

«Допускается сокращённая форма модуля, состоящая только из тела модуля,

если она используется, предполагается, что заголовок имеет вид module Main(main) where».

Параметр -main-is в GHC можно использовать, чтобы изменить имя точки входа верхнего уровня с main на любую другую переменную. При компиляции главного модуля, если параметр -main-is использовался для переименования точки входа по умолчанию, GHC также укажет альтернативное имя в списке экспорта по умолчанию.

Рассмотрим следующую программу:

-- file: Main.hs
program :: IO ()
program = return ()

GHC успешно скомпилирует этот модуль с ghc -main-is Main.program Main.hs, поскольку список экспорта по умолчанию будет содержать program вместо main, как обычно требует Отчёт Haskell.

Это изменение применяется только к главному модулю. Другие модули по-прежнему будут экспортировать main из списка экспорта по умолчанию независимо от флага -main-is. Это позволяет использовать -main-is с существующими модулями, которые экспортируют main через список экспорта по умолчанию, даже если -main-is указывает на другую точку входа, как в этом примере (скомпилированном с -main-is MainWrapper.program).

-- file MainWrapper.hs
module MainWrapper where
import Main

program :: IO ()
program = putStrLn "Redirecting..." >> main

-- file Main.hs
main :: IO ()
main = putStrLn "I am main."

16.1.1.7. Система модулей и интерфейсные файлы

GHC требует использования файлов hs-boot для разрыва рекурсивных циклов между взаимно рекурсивными модулями, как описано в разделе Взаимно рекурсивные модули и файлы hs-boot. Это скорее недостаток, чем ошибка: в Отчёте Haskell сказано (раздел 5.7)

«В зависимости от используемой реализации Haskell для раздельной компиляции взаимно рекурсивных модулей может потребоваться дополнительная информация в импортируемых модулях, чтобы на них можно было ссылаться до их компиляции. Для обработки взаимной рекурсии могут понадобиться явные сигнатуры типов для всех экспортируемых значений. Точные детали раздельной компиляции в этом Отчёте не определяются».

16.1.1.8. Числа, базовые типы и встроенные классы

Num superclasses

Класс Num не имеет суперклассов Show или Eq.

Чтобы код работал и с Haskell98/Haskell2010, и с GHC:

  • Для каждого экземпляра Num типа создавайте также экземпляры Show и Eq, и
  • Для каждой функции, экземпляра или класса с ограничением Num t задавайте также ограничения Show t и Eq t.
Bits superclass

Класс Bits не имеет суперкласса Num. Поэтому у него нет методов по умолчанию для методов bit, testBit и popCount.

Чтобы код работал и с Haskell 2010, и с GHC:

  • Для каждого экземпляра Bits типа создавайте также экземпляр Num, и
  • Для каждой функции, экземпляра или класса с ограничением Bits t задавайте также ограничение Num t, и
  • Всегда определяйте методы bit, testBit и popCount в экземплярах Bits.
Read class methods

У класса Read есть два дополнительных метода — readPrec и readListPrec, которых нет в Haskell 2010, поскольку они опираются на тип данных ReadPrec, для которого требуется расширение RankNTypes. GHC также выводит экземпляры Read, реализуя readPrec вместо readsPrec, и использует реализацию метода readsPrec по умолчанию, определённую через readPrec. GHC добавляет эти два дополнительных метода просто потому, что ReadPrec эффективнее, чем ReadS (тип, на котором основан readsPrec).

Monad superclass

У класса Monad есть суперкласс Applicative. Нельзя написать экземпляры Monad, которые работали бы в GHC и одновременно в реализации Haskell 2010, не определяющей Applicative.

Дополнительные экземпляры

Определены следующие дополнительные экземпляры:

instance Functor ((->) r)
instance Monad ((->) r)
instance Functor ((,) a)
instance Functor (Either a)
instance Monad (Either e)
Повторно заданные элементы массива не проверяются

Этот фрагмент кода должен вызывать фатальную ошибку, но этого не происходит:

main = print (array (1,1) [(1,2), (1,3)])

Реализация array в GHC берёт значение ячейки массива из последней пары (индекс, значение) в списке и не проверяет наличие дубликатов. Причина исключительно в эффективности.

16.1.1.9. Поддержка в Prelude

splitAt semantics

Data.List.splitAt строже, чем указано в отчёте. В частности, в отчёте указано, что

splitAt n xs = (take n xs, drop n xs)

из чего следует, что

splitAt undefined undefined = (undefined, undefined)

однако реализация GHC строго вычисляет первый аргумент, поэтому

splitAt undefined [] = undefined
Showing records

Определение Show в Haskell 2010 предписывает, чтобы в строковом представлении присутствовали только те скобки, которые необходимы для однозначного разбора результата. По историческим причинам экземпляры Show, выводимые GHC, содержат скобки вокруг записей, хотя синтаксис записей имеет более высокий приоритет, чем применение функции; например:

data Hello = Hello { aField :: Int } deriving (Show)

-- GHC produces...
show (Just (Hello {aField=42})) == "Just (Hello {aField=42})"

-- whereas Haskell 2010 calls for...
show (Just (Hello {aField=42})) == "Just Hello {aField=42}"
Reading integers

Реализация класса Read для целочисленных типов в GHC поддерживает шестнадцатеричные, восьмеричные и двоичные литералы (в коде отчёта Haskell 98 такой возможности нет). Например,

read "0xf00" :: Int

работает в GHC.

Это сделано для согласованности с синтаксисом языка. Haskell98 поддерживает шестнадцатеричный и восьмеричный форматы, а GHC2021 также поддерживает двоичный формат.

isAlpha

Определение isAlpha в Haskell 98 выглядит так:

isAlpha c = isUpper c || isLower c

Реализация GHC отличается от определения в Haskell 98 тем, что isAlpha считает буквенными также символы Unicode, которые не относятся ни к верхнему, ни к нижнему регистру.

hGetContents

При ленивом вводе-выводе возникает исключение при обнаружении ошибки, в отличие от спецификации Haskell 98, которая требует отбрасывать ошибки (см. раздел 21.2.2 отчёта Haskell 98). Возникает обычное исключение ввода-вывода, которое возникло бы при выполнении неудачной операции ввода-вывода в монаде IO; его можно перехватить с помощью System.IO.Error.catch или Control.Exception.catch.

16.1.1.10. Интерфейс внешних функций

hs_init(), hs_exit()

Спецификация FFI требует, чтобы реализация поддерживала повторную инициализацию после завершения работы с помощью hs_exit(), однако в настоящее время GHC этого не поддерживает. См. #13693.

16.1.2. Интерпретация GHC неопределённого поведения в Haskell 98 и Haskell 2010

В этом разделе описана трактовка GHC различных вопросов, оставленных неопределёнными или зависящими от реализации в Haskell 98.

Char

В соответствии со стандартом ISO-10646 maxBound :: Char в GHC имеет значение 0x10FFFF.

Int

В GHC тип Int соответствует размеру адреса в архитектуре хоста; другими словами, он хранит 32 бита на 32-разрядной машине и 64 бита на 64-разрядной машине.

Арифметические операции над Int выполняются без проверки переполненияInt, поэтому все операции над Int выполняются по модулю 2⟨n⟩, где ⟨n⟩ — размер типа Int в битах.

При преобразовании в Int тип fromInteger (а значит, и fromIntegral) является особым случаем. Значение fromIntegral x :: Int определяется как младшие ⟨n⟩ бит (abs x), умноженные на знак x (в ⟨n⟩-разрядной арифметике с дополнением до двух). Такое поведение выбрано, чтобы, например, запись 0xffffffff :: Int сохраняла битовый шаблон результирующего Int.

Согласно (тщательному прочтению) отчёта Haskell, отрицательные литералы, например -3, означают Prelude.negate (Prelude.fromInteger 3). Таким образом, -2147483648 означает negate (fromInteger 2147483648). Поскольку fromInteger берёт младшие 32 бита представления, fromInteger (2147483648::Integer), вычисленное с типом Int, равно -2147483648::Int. Затем операция negate приводит к переполнению, но оно не проверяется, поэтому negate (-2147483648::Int) — это просто -2147483648. Короче говоря, minBound::Int можно записать как литерал с ожидаемым значением (но в общем случае это не гарантируется).

Функция fromIntegral также сохраняет битовые шаблоны при преобразовании между целочисленными типами фиксированного размера (Int8, Int16, Int32, Int64 и беззнаковыми вариантами Word); см. модули Data.Int и Data.Word в документации библиотеки.

Арифметические операции с плавающей точкой без проверки

Операции с числами Float и Double выполняются без проверки переполнения, потери значимости и других неприятных ситуаций. (Однако следует учитывать, что некоторые архитектуры перехватывают переполнение с плавающей точкой и потерю точности и сообщают об исключении с плавающей точкой, что, вероятно, приводит к завершению программы.)

Поддержка кортежей большого размера

Отчёт Haskell требует от реализаций предоставлять типы кортежей и соответствующие стандартные экземпляры только для кортежей размером до 15. GHC ограничивает размер типов кортежей значением 64 и предоставляет экземпляры Eq, Ord, Bounded, Read, Show и Ix для кортежей размером до 15.

16.2. Известные ошибки и недостатки

В системе отслеживания ошибок перечислены ошибки, о которых сообщили в GHC, но которые ещё не исправлены: см. систему отслеживания ошибок GHC. Помимо них, в GHC также есть следующие известные ошибки и недостатки. Эти ошибки носят более постоянный характер; маловероятно, что какие-либо из них будут исправлены в ближайшее время.

16.2.1. Ошибки в GHC

  • Среда выполнения GHC реализует кооперативную многозадачность: переключение контекста потенциально может происходить только при выделении памяти программой. Это означает, что программы, которые не выделяют память, могут никогда не переключать контекст. Особенно это касается программ, использующих STM: они могут зайти в тупик, обнаружив несогласованное состояние. Подробнее см. #367.

    Если вы столкнулись с этой проблемой, можно скомпилировать затронутый модуль с помощью -fno-omit-yields (см. -f*: независимые от платформы флаги). Этот флаг гарантирует вставку точек передачи управления в начало каждой функции (за счёт некоторого снижения производительности).

  • GHC не позволяет объявлять тип данных с контекстом, в котором упоминаются переменные типов, не являющиеся параметрами типа данных. Например:

    data C a b => T a = MkT a
    

    так что тип MkT имеет вид

    MkT :: forall a b. C a b => a -> T a
    

    В принципе, при подходящем объявлении класса с функциональной зависимостью этот тип может быть однозначным; однако GHC всё равно его отвергает. Переменные типов, упомянутые в контексте объявления типа данных, должны входить в число параметров типа данных.

  • Инлайнер GHC можно привести к бесконечной работе, используя стандартный способ кодирования рекурсии через тип данных:

    data U = MkU (U -> Bool)
    
    russel :: U -> Bool
    russel u@(MkU p) = not $ p u
    
    x :: Bool
    x = russel (MkU russel)
    

    Бесконечная работа приводит к следующему сообщению:

    ghc: panic! (the 'impossible' happened)
      (GHC version 8.2.1 for x86_64-unknown-linux):
        Simplifier ticks exhausted
      When trying UnfoldingDone x_alB
      To increase the limit, use -fsimpl-tick-factor=N (default 100)
    

    причём сообщение о сбое появляется независимо от того, насколько велико заданное вами значение -fsimpl-tick-factor.

    Мы не встречали других классов программ, кроме этого искусственного примера, из-за которых GHC зацикливался бы, а исправление проблемы привело бы к дополнительным затратам при каждой компиляции. Поэтому ошибка остаётся неисправленной. Подробнее об этом см. в статье Секреты инлайнера GHC.

  • На 32-разрядных платформах x86 при использовании собственного генератора кода параметр -fexcess-precision всегда включён. Это означает, что вычисления с плавающей точкой недетерминированы: в зависимости от способа компиляции программы (например, от настроек оптимизации) некоторые вычисления могут выполняться с точностью 80 бит вместо предполагаемой точности 32 или 64 бита. Результаты вычислений с плавающей точкой могут различаться при включённой оптимизации. В худшем случае нарушается ссылочная прозрачность, поскольку, например, let x = E1 in E2 может вычисляться в значение, отличное от E2[E1/x].

    Один из способов обойти эту проблему — использовать параметр -msse2 (см. Флаги для конкретных платформ), который генерирует код для использования набора инструкций SSE2 вместо набора инструкций x87. Код SSE2 использует правильную точность для всех операций с плавающей точкой и поэтому даёт детерминированные результаты. Однако учтите, что это работает только на процессорах с поддержкой SSE2 (Intel Pentium 4 или AMD Athlon 64 и новее), поэтому этот параметр не включён по умолчанию. Библиотеки, поставляемые с GHC, вероятно, собраны без этого параметра, если только вы не собирали GHC самостоятельно.

  • Оптимизация state hack может приводить к неочевидным изменениям порядка вычислений, которые способны скрывать исключения даже при использовании -fpedantic-bottoms (см., например, #7411). Например,

    import Control.Exception
    import Control.DeepSeq
    main = do
        evaluate (('a' : undefined) `deepseq` return () :: IO ())
        putStrLn "Hello"
    

    Компиляция этой программы с помощью -O приводит к выводу Hello, несмотря на то, что вычисление evaluate должно было завершиться дном. Компиляция с помощью -O -fno-state-hack приводит к ожидаемому исключению.

  • Программы, скомпилированные с помощью -fdefer-type-errors, могут завершаться с ошибкой несколько раньше, чем можно было бы ожидать. Например,

    {-# OPTIONS_GHC -fdefer-type-errors #-}
    main = do
      putStrLn "Hi there."
      putStrLn True
    

    не выведет ничего, несмотря на то, что выражение с неверным типом стоит после корректного putStrLn "Hi there.". См. #11197.

  • Несмотря на внешнее сходство, * и Constraint на самом деле не являются разными видами в внутреннем представлении компилятора и могут быть объединены, что приводит к неожиданным результатам. Один из примеров см. в #11715.
  • Из-за ограничений набора инструментов мы не можем обеспечить полную поддержку путей Unicode в Windows. В Windows поддерживаются символы Latin-1. Подробнее см. #12971.
  • Проверка типов может отклонять некоторые программы с оператором ($), которые принимаются при использовании явных скобок. Подробнее см. #26543.

16.2.2. Ошибки в GHCi (интерактивной среде GHC)

  • GHCi не учитывает объявление default в модуле, в области видимости которого вы находитесь. Вместо этого для выражений, вводимых в командной строке, всегда используется поведение по умолчанию для типов по умолчанию, то есть default(Int,Double).

    Было бы лучше, если бы GHCi сохранял настройки по умолчанию для каждого модуля и использовал настройки «текущего» модуля (каков бы он ни был).

  • В Windows есть ошибка GNU ld/BFD, из-за которой создаются некорректные объектные файлы PE с числом перемещений больше 0xffff. Когда GHCi пытается загрузить пакет, затронутый этой ошибкой, появляется сообщение следующего вида

    Loading package javavm ... linking ... WARNING: Overflown relocation field (# relocs found: 30765)
    

    Когда мы в последний раз проверяли, эта ошибка всё ещё не была исправлена в кодовой базе BFD, и заметного интереса к её исправлению не было, хотя мы сообщили о ней ещё примерно в 2001 году.

    Обходной путь — разделить файлы .o, составляющие пакет, на два или более файла .o, примерно так же, как это сделано в пакете base.

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.14.1/docs/users_guide/bugs.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API