hydra-0.15.0: src/main/haskell/Hydra/ExtGeneration.hs
-- | Entry point for code generation in hydra-ext; provides additional sources and coders not found in hydra-haskell.
module Hydra.ExtGeneration (
module Hydra.ExtGeneration,
module Hydra.Sources.Ext,
module Hydra.Generation,
module Hydra.Haskell.Generation,
module Hydra.Sources.All,
) where
import Hydra.Kernel
import Hydra.Generation
import Hydra.Haskell.Generation
import Hydra.Sources.Ext
import Hydra.Sources.All
import Hydra.Cpp.Language (cppLanguage)
import Hydra.Java.Language
import Hydra.Json.Schema.Language (jsonSchemaLanguage)
import Hydra.Protobuf.Language (protobufLanguage)
import Hydra.Python.Language
import Hydra.Pegasus.Language (pdlLanguage)
import Hydra.Scala.Language (scalaLanguage)
import Hydra.Cpp.Coder (moduleToCpp)
import Hydra.Graphql.Coder (moduleToGraphql)
import Hydra.Graphql.Language (graphqlLanguage)
import Hydra.Java.Coder (moduleToJava)
import Hydra.Json.Schema.Coder
import Hydra.Pegasus.Coder (moduleToPdl)
import Hydra.Protobuf.Coder (moduleToProtobuf)
import Hydra.Python.Coder (moduleToPython)
import Hydra.Rust.Coder (moduleToRust)
import Hydra.Rust.Language (rustLanguage)
import Hydra.Wasm.Coder (moduleToWasm)
import Hydra.Wasm.Language (wasmLanguage)
import Hydra.Lisp.Coder (moduleToLisp)
import Hydra.Lisp.Language (lispLanguage)
import Hydra.Lisp.Serde (programToExpr)
import qualified Hydra.Lisp.Syntax as LispSyntax
import qualified Hydra.Serialization as Serialization
import qualified Hydra.Names as Names
import qualified Hydra.Util as Util
import Hydra.Scala.Coder (moduleToScala)
import Hydra.Coq.GenerateDriver (moduleToCoq)
import Hydra.Coq.Generate (globalFieldMapping, globalConstructorCounts, globalAmbiguousNames, globalSanitizedAccessors)
import Hydra.Coq.Language (coqLanguage)
import Control.Monad (when)
import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Set as Set
import qualified System.Directory as SD
import qualified System.FilePath as FP
import qualified System.IO as SIO
-- | Options for JSON Schema code generation (was previously in Staging module)
data JsonSchemaOptions = JsonSchemaOptions {
jsonSchemaOptionsShortNames :: Bool
}
-- | Generate C++ source files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
writeCpp :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeCpp = generateSources moduleToCpp cppLanguage True False False False
-- | Generate GraphQL source files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
writeGraphql :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeGraphql = generateSources moduleToGraphql graphqlLanguage True False False False
-- | Generate GraphQL source files without type adaptation.
-- Useful when the source types include constructs (like forall) that the adapter doesn't handle,
-- but the coder can encode directly.
writeGraphqlRaw :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeGraphqlRaw = generateSources moduleToGraphql graphqlLanguage False False False False
-- | Generate Java source files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
-- Note: Java uses doHoistPolymorphicLetBindings=True to hoist polymorphic let bindings to class level
writeJava :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeJava = generateSources moduleToJava javaLanguage True True False True
-- | Generate JSON Schema files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
writeJsonSchema :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeJsonSchema = generateSources (moduleToJsonSchema (JsonSchemaOptions True)) jsonSchemaLanguage True False False False
-- | Generate PDL (Pegasus) source files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
writePdl :: FP.FilePath -> [Module] -> [Module] -> IO Int
writePdl = generateSources moduleToPdl pdlLanguage True False False False
-- | Generate Protocol Buffers source files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
writeProtobuf :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeProtobuf = generateSources moduleToProtobuf protobufLanguage True False False False
-- | Generate Python source files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
-- Note: Python uses doHoistCaseStatements=True to hoist case statements to let bindings
writePython :: FP.FilePath -> [Module] -> [Module] -> IO Int
writePython = generateSources moduleToPython pythonLanguage True True True False
-- | Generate Rust source files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
writeRust :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeRust = generateSources moduleToRust rustLanguage True False False False
-- | Generate Coq (.v) source files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
writeCoq :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeCoq basePath universeModules modulesToGenerate =
let allMods = universeModules ++ modulesToGenerate
fm = globalFieldMapping allMods
cc = globalConstructorCounts allMods
-- Hydra primitive library functions are not Hydra modules — they are
-- implemented host-side (in this case as Coq Definitions/Axioms in
-- `hydra/lib/*.v`) and are therefore invisible to `globalAmbiguousNames`.
-- Collisions like `hydra.lib.maybes.maybe` vs `hydra.show.core.maybe`
-- only manifest at Coq's import level. Inject the lib primitive names
-- into the ambiguous set so that cross-module references in the
-- non-lib source (`hydra.show.core.maybe`) stay fully qualified.
an = Set.union (globalAmbiguousNames allMods) coqLibPrimitiveNames
sa = globalSanitizedAccessors allMods
in generateSources (moduleToCoq fm cc an sa) coqLanguage True False False False basePath universeModules modulesToGenerate
-- | Names of Hydra primitive library functions exported by `hydra.lib.*`
-- Coq modules. These are invisible to `globalAmbiguousNames` (which walks
-- Hydra module definitions) because the lib modules are implemented
-- host-side in hand-written Coq. Used to disambiguate references like
-- `hydra.show.core.maybe` that would otherwise collide with bare imports
-- of the lib modules.
coqLibPrimitiveNames :: Set.Set String
coqLibPrimitiveNames = Set.fromList [
"abs","acos","acosh","add","addFloat64","alter","and","apply","asin",
"asinh","at","atan","atan2","atanh","bigfloatToBigint","bigfloatToFloat32",
"bigfloatToFloat64","bigintToBigfloat","bigintToDecimal","bigintToInt16",
"bigintToInt32","bigintToInt64","bigintToInt8","bigintToUint16",
"bigintToUint32","bigintToUint64","bigintToUint8","bimap","binaryToBytes",
"binaryToString","bind","cases","cat","cat2","ceiling","charAt","compare",
"compose","concat","concat2","cons","contains","cos","cosh",
"decimalToBigint","decimalToFloat32","decimalToFloat64","delete",
"difference","div","drop","dropWhile","e","either","elem","elems","empty",
"equal","even","exp","filter","filterWithKey","find","findAll",
"findWithDefault","first","float32ToBigfloat","float32ToDecimal",
"float64ToBigfloat","float64ToDecimal","floor","foldl","foldr","fromJust",
"fromLeft","fromList","fromMaybe","fromRight","group","gt","gte","head",
"identity","ifElse","init","insert","int16ToBigint","int32ToBigint",
"int64ToBigint","int8ToBigint","intercalate","intersection","intersperse",
"isAlphaNum","isJust","isLeft","isLower","isNothing","isRight","isSpace",
"isUpper","keys","last","lefts","length","lines","log","logBase","lookup",
"lt","lte","map","mapKeys","mapList","mapMaybe","mapSet","matches","max",
"maybe","maybeAt","maybeCharAt","maybeDiv","maybeHead","maybeInit",
"maybeLast","maybeMod","maybePred","maybeRem","maybeSucc","maybeTail",
"member","min","mod","mul","mulFloat64","negate","negateFloat64","not",
"nub","null","odd","or","partition","partitionEithers","pi","pow","pred",
"pure","range","readBigfloat","readBigint","readBoolean","readDecimal",
"readFloat32","readFloat64","readInt16","readInt32","readInt64","readInt8",
"readString","readUint16","readUint32","readUint64","readUint8","rem",
"replace","replaceAll","replicate","reverse","rights","round",
"roundBigfloat","roundFloat32","roundFloat64","safeHead","second",
"showBigfloat","showBigint","showBoolean","showDecimal","showFloat32",
"showFloat64","showInt16","showInt32","showInt64","showInt8","showString",
"showUint16","showUint32","showUint64","showUint8","signum","sin",
"singleton","sinh","size","sort","sortOn","span","split","splitOn","sqrt",
"stringToBinary","sub","subFloat64","succ","tail","take","tan","tanh",
"toList","toLower","toUpper","transpose","truncate","uint16ToBigint",
"uint32ToBigint","uint64ToBigint","uint8ToBigint","union","unions",
"unlines","zip","zipWith"]
-- | Wrap moduleToLisp for a specific dialect
moduleToLispDialect
:: LispSyntax.Dialect -> String
-> Module -> [Definition] -> Context -> Graph
-> Either Error (M.Map FilePath String)
moduleToLispDialect dialect ext mod defs cx g =
case moduleToLisp dialect mod defs cx g of
Left err -> Left err
Right program ->
let code = Serialization.printExpr (Serialization.parenthesize (programToExpr program))
caseConvention = case dialect of
LispSyntax.DialectClojure -> Util.CaseConventionCamel
_ -> Util.CaseConventionLowerSnake
filePath = Names.namespaceToFilePath caseConvention (FileExtension ext) (moduleNamespace mod)
in Right (M.singleton filePath code)
writeClojure :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeClojure = generateSources (moduleToLispDialect LispSyntax.DialectClojure "clj") lispLanguage True False False False
writeScheme :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeScheme = generateSources (moduleToLispDialect LispSyntax.DialectScheme "scm") lispLanguage True False False False
writeCommonLisp :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeCommonLisp = generateSources (moduleToLispDialect LispSyntax.DialectCommonLisp "lisp") lispLanguage True False False False
writeEmacsLisp :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeEmacsLisp = generateSources (moduleToLispDialect LispSyntax.DialectEmacsLisp "el") lispLanguage True False False False
-- | Generate Scala source files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
--
-- After generation, walks the output directory and wraps long lines in each
-- generated .scala file via 'wrapLongScalaLines'. This avoids the Scala
-- compiler's memory issues on extremely long single-line expressions, and
-- replaces the previously-external break-long-lines.py post-processor.
writeScala :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeScala basePath universeMods mods = do
n <- generateSources moduleToScala scalaLanguage True True False False basePath universeMods mods
wrapLongLinesInScalaTree basePath
return n
-- | Generate WebAssembly text format (WAT) files from modules.
-- First argument: output directory
-- Second argument: universe modules (all modules for type/term resolution)
-- Third argument: modules to transform and generate
writeWasm :: FP.FilePath -> [Module] -> [Module] -> IO Int
writeWasm = generateSources moduleToWasm wasmLanguage True False False False
-- | Soft maximum line length for generated source files in any target
-- language. Lines longer than this are broken at the first eligible break
-- point; segments below this length are left intact even if they could be
-- broken further. The target is readability first; the secondary goal is
-- to keep individual lines short enough that downstream compilers do not
-- blow their stacks on a single megaline.
maxLineLength :: Int
maxLineLength = 120
-- | Once the current segment exceeds this length, the next ',' is a break
-- point. Smaller than 'maxLineLength' so that the post-break continuation
-- has room before the next forced wrap.
commaBreakThreshold :: Int
commaBreakThreshold = 80
-- | Once the current segment exceeds this length, '=>' immediately after
-- a ')' becomes a break point.
arrowBreakThreshold :: Int
arrowBreakThreshold = 60
-- | Walk a directory tree and wrap long lines in every .scala file.
wrapLongLinesInScalaTree :: FP.FilePath -> IO ()
wrapLongLinesInScalaTree dir = do
exists <- SD.doesDirectoryExist dir
when exists $ do
entries <- SD.listDirectory dir
mapM_ visit entries
where
visit name = do
let path = dir FP.</> name
isDir <- SD.doesDirectoryExist path
if isDir
then wrapLongLinesInScalaTree path
else when (FP.takeExtension path == ".scala") $ do
-- Read strictly to release the file handle before writing back.
contents <- SIO.withFile path SIO.ReadMode $ \h -> do
cs <- SIO.hGetContents h
length cs `seq` return cs
let wrapped = wrapLongScalaText contents
when (wrapped /= contents) (writeFile path wrapped)
-- | Apply line-wrapping to every line in a Scala source file. Lines under
-- the max length pass through unchanged; long lines are broken at safe
-- points (commas outside string literals, when the segment is long).
wrapLongScalaText :: String -> String
wrapLongScalaText = unlines . fmap wrapLongScalaLine . lines
-- | Break a single long line at safe break points. If the line is short
-- enough, returns it unchanged. Otherwise walks character-by-character,
-- tracking string-literal state, and breaks at:
-- * a ',' after the current segment exceeds 'commaBreakThreshold' chars
-- * '=>' immediately after ')' (lambda body), after the current segment
-- exceeds 'arrowBreakThreshold' chars
wrapLongScalaLine :: String -> String
wrapLongScalaLine line
| length line <= maxLineLength = line
| otherwise =
let indent = takeWhile (== ' ') line
commaCont = indent ++ " "
arrowCont = indent ++ " "
(segments, lastSeg) = scan line "" [] False '\NUL' commaCont arrowCont
-- Drop a whitespace-only trailing segment: when scan breaks at the
-- final comma in the input, the residual `cur` is just `commaCont`
-- (the indent prefix) with no real content. Emitting it adds a
-- blank-but-indented line at the end, which the post-strip used to
-- silently fix; with the strip removed, we filter here at source.
isWhitespaceOnly = all (== ' ')
allSegments = if null lastSeg || isWhitespaceOnly lastSeg
then segments
else segments ++ [lastSeg]
in if length allSegments <= 1
then line
else L.intercalate "\n" allSegments
where
scan :: String -> String -> [String] -> Bool -> Char -> String -> String -> ([String], String)
scan [] cur acc _ _ _ _ = (acc, cur)
scan (c:rest) cur acc inString prevChar commaCont arrowCont =
let cur' = cur ++ [c]
inString' = if c == '"' && prevChar /= '\\'
then not inString
else inString
isArrowStart = not inString' && c == '=' && case rest of
('>':_) -> True
_ -> False
arrowAfterParen = isArrowStart && prevChar == ')' && length cur' > arrowBreakThreshold
shouldBreakComma = c == ',' && not inString' && length cur' > commaBreakThreshold
in if arrowAfterParen
then -- consume '>', emit segment "...=>", continue with arrow indent
let cur'' = cur' ++ ['>']
rest' = drop 1 rest
in scan rest' arrowCont (acc ++ [cur'']) inString' '>' commaCont arrowCont
else if shouldBreakComma
then scan rest commaCont (acc ++ [cur']) inString' c commaCont arrowCont
else scan rest cur' acc inString' c commaCont arrowCont