@@ -10,10 +10,43 @@ import qualified Data.Text as Text
1010import qualified Data.Text.Lazy as TL
1111import qualified Data.Text.Lazy.Encoding as TL
1212import L4.Syntax (Type' (.. ), Resolved , getUnique )
13- import L4.TypeCheck.Environment (booleanUnique )
13+ import L4.TypeCheck.Environment (booleanUnique , dateUnique )
1414import Backend.Api (TraceLevel (.. ))
1515import Backend.MaybeLift (liftTypeToMaybe )
1616
17+ -- | Check if a type is exactly DATE (not MAYBE DATE, LIST OF DATE, etc.)
18+ -- Used to determine if we need TODATE conversion from JSON strings
19+ isDateType :: Type' Resolved -> Bool
20+ isDateType (TyApp _ name [] ) = getUnique name == dateUnique
21+ isDateType _ = False
22+
23+ -- | Check if a type contains DATE at its core (unwrapping MAYBE)
24+ -- e.g., DATE -> True, MAYBE DATE -> True, MAYBE (MAYBE DATE) -> True
25+ -- but LIST OF DATE -> False (we don't handle nested structures yet)
26+ containsDateType :: Type' Resolved -> Bool
27+ containsDateType ty = isDateType (unwrapMaybe ty)
28+ where
29+ -- Unwrap MAYBE layers to get to the core type
30+ -- Note: We can't easily check for maybeUnique here without importing it,
31+ -- so we use a heuristic: if it's a TyApp with one type argument, check inner
32+ unwrapMaybe :: Type' Resolved -> Type' Resolved
33+ unwrapMaybe (TyApp _ _ [inner]) = unwrapMaybe inner
34+ unwrapMaybe t = t
35+
36+ -- | Quote an identifier with backticks if it contains spaces or special characters
37+ -- L4 requires backticks for identifiers with spaces
38+ quoteIdent :: Text -> Text
39+ quoteIdent name
40+ | Text. any (== ' ' ) name = " `" <> name <> " `"
41+ | otherwise = name
42+
43+ -- | Create a safe unwrapped variable name
44+ -- For names with spaces, creates a backtick-quoted identifier like `unwrapped_tranche number`
45+ unwrappedVar :: Text -> Text
46+ unwrappedVar name
47+ | Text. any (== ' ' ) name = " `unwrapped_" <> name <> " `"
48+ | otherwise = " unwrapped_" <> name
49+
1750-- | Result of code generation
1851data GeneratedCode = GeneratedCode
1952 { generatedWrapper :: Text
@@ -62,13 +95,13 @@ generateEvalWrapper funName givenParams assumeParams inputJson traceLevel = do
6295 isBooleanType :: Type' Resolved -> Bool
6396 isBooleanType (TyApp _ name [] ) = getUnique name == booleanUnique
6497 isBooleanType _ = False
65- -- Annotate GIVEN params with their boolean status and mark as GIVEN
66- givenParamInfo = map (\ (name, ty) -> ((name, ty), isBooleanType ty, True )) givenParams
67- -- Annotate ASSUME params with their boolean status and mark as ASSUME
68- assumeParamInfo = map (\ (name, ty) -> ((name, ty), isBooleanType ty, False )) assumeParams
98+ -- Annotate GIVEN params with (type info, isBoolean, isGiven, isDate)
99+ givenParamInfo = map (\ (name, ty) -> ((name, ty), isBooleanType ty, True , containsDateType ty )) givenParams
100+ -- Annotate ASSUME params with (type info, isBoolean, isGiven, isDate)
101+ assumeParamInfo = map (\ (name, ty) -> ((name, ty), isBooleanType ty, False , containsDateType ty )) assumeParams
69102 allParamInfo = givenParamInfo <> assumeParamInfo
70103 -- We always need prelude for fromMaybe (all booleans use it)
71- hasBooleans = any (\ (_, isB, _) -> isB) allParamInfo
104+ hasBooleans = any (\ (_, isB, _, _ ) -> isB) allParamInfo
72105 in Right GeneratedCode
73106 { generatedWrapper = Text. unlines $
74107 [ " "
@@ -106,7 +139,9 @@ generateInputRecordLifted params = Text.unlines $
106139 let indent = if idx == 0 then " " else " , "
107140 -- Lift ALL types to MAYBE
108141 tyText = liftTypeToMaybe ty
109- in indent <> name <> " IS A " <> tyText
142+ -- Quote identifiers with spaces
143+ quotedName = quoteIdent name
144+ in indent <> quotedName <> " IS A " <> tyText
110145
111146-- | Generate typed decoder function
112147generateDecoder :: Text
@@ -134,11 +169,11 @@ escapeAsL4String val =
134169-- GIVEN params are passed as function arguments.
135170-- ASSUME params are injected as LET bindings before the function call.
136171--
137- -- Type signature: [((name, type), isBoolean, isGiven)]
172+ -- Type signature: [((name, type), isBoolean, isGiven, isDate )]
138173generateEvalDirectiveLiftedWithAssumes
139174 :: Text
140- -> [((Text , Type' Resolved ), Bool , Bool )] -- ^ GIVEN params: ((name, type), isBoolean, isGiven=True)
141- -> [((Text , Type' Resolved ), Bool , Bool )] -- ^ ASSUME params: ((name, type), isBoolean, isGiven=False)
175+ -> [((Text , Type' Resolved ), Bool , Bool , Bool )] -- ^ GIVEN params: ((name, type), isBoolean, isGiven=True, isDate )
176+ -> [((Text , Type' Resolved ), Bool , Bool , Bool )] -- ^ ASSUME params: ((name, type), isBoolean, isGiven=False, isDate )
142177 -> TraceLevel
143178 -> Text
144179generateEvalDirectiveLiftedWithAssumes funName givenParamInfo assumeParamInfo traceLevel =
@@ -150,33 +185,38 @@ generateEvalDirectiveLiftedWithAssumes funName givenParamInfo assumeParamInfo tr
150185 allParams = givenParamInfo <> assumeParamInfo
151186
152187 -- Separate boolean and non-boolean params (for unwrapping logic)
153- nonBoolAllParams = filter (\ (_, isB, _) -> not isB) allParams
188+ nonBoolAllParams = filter (\ (_, isB, _, _ ) -> not isB) allParams
154189
155190 -- Generate argument expressions for GIVEN params only (in original order)
156191 -- Booleans use fromMaybe FALSE directly
157192 -- Non-booleans reference unwrapped variable names
193+ -- Date types need conversion via dateConvertedVar
158194 givenArgExprs = map snd $ sortOn fst $
159- [(idx, expr) | (idx, ((name, _), True , _)) <- zip [0 :: Int .. ] givenParamInfo
160- , let expr = " (fromMaybe FALSE (args's " <> name <> " ))" ] ++
161- [(idx, expr) | (idx, ((name, _), False , _)) <- zip [0 :: Int .. ] givenParamInfo
162- , let expr = " unwrapped_ " <> name]
195+ [(idx, expr) | (idx, ((name, _), True , _, _ )) <- zip [0 :: Int .. ] givenParamInfo
196+ , let expr = " (fromMaybe FALSE (args's " <> quoteIdent name <> " ))" ] ++
197+ [(idx, expr) | (idx, ((name, _), False , _, isDate )) <- zip [0 :: Int .. ] givenParamInfo
198+ , let expr = if isDate then dateConvertedVar name else unwrappedVar name]
163199
164200 -- Build function call with only GIVEN params as arguments
201+ -- Quote function name if it contains spaces
202+ quotedFunName = quoteIdent funName
165203 functionCall = if null givenArgExprs
166- then funName
167- else funName <> " " <> Text. unwords givenArgExprs
204+ then quotedFunName
205+ else quotedFunName <> " " <> Text. unwords givenArgExprs
168206
169207 -- Wrap function call with LET bindings for ASSUME params
170208 -- Boolean ASSUMEs use fromMaybe FALSE
171209 -- Non-boolean ASSUMEs use unwrapped_ variables
210+ -- Date ASSUMEs use dateConverted_ variables
172211 wrapWithAssumes :: Text -> Text
173212 wrapWithAssumes innerExpr = foldr wrapOne innerExpr assumeParamInfo
174213 where
175- wrapOne ((name, _), isBoolean, _) expr =
176- let valueExpr = if isBoolean
177- then " (fromMaybe FALSE (args's " <> name <> " ))"
178- else " unwrapped_" <> name
179- in " LET " <> name <> " = " <> valueExpr <> " IN " <> expr
214+ wrapOne ((name, _), isBoolean, _, isDate) expr =
215+ let quotedName = quoteIdent name
216+ valueExpr = if isBoolean
217+ then " (fromMaybe FALSE (args's " <> quotedName <> " ))"
218+ else if isDate then dateConvertedVar name else unwrappedVar name
219+ in " LET " <> quotedName <> " = " <> valueExpr <> " IN " <> expr
180220
181221 -- The innermost expression (wrapped function call in JUST)
182222 innerCall = " JUST (" <> wrapWithAssumes functionCall <> " )"
@@ -201,29 +241,53 @@ generateEvalDirectiveLiftedWithAssumes funName givenParamInfo assumeParamInfo tr
201241
202242-- | Generate nested CONSIDER for unwrapping non-boolean MAYBE values,
203243-- with LET bindings for ASSUME params at the innermost level.
244+ -- For DATE types, adds an additional CONSIDER to convert string -> DATE via TODATE.
204245generateNestedConsiderWithAssumes
205- :: [((Text , Type' Resolved ), Bool , Bool )] -- ^ Non-boolean params to unwrap
206- -> Text -- ^ Function call (GIVEN args only)
207- -> [((Text , Type' Resolved ), Bool , Bool )] -- ^ ASSUME params for LET bindings
208- -> Int -- ^ Indentation level
246+ :: [((Text , Type' Resolved ), Bool , Bool , Bool )] -- ^ Non-boolean params to unwrap (name, type, isBoolean, isGiven, isDate)
247+ -> Text -- ^ Function call (GIVEN args only)
248+ -> [((Text , Type' Resolved ), Bool , Bool , Bool )] -- ^ ASSUME params for LET bindings
249+ -> Int -- ^ Indentation level
209250 -> [Text ]
210251generateNestedConsiderWithAssumes [] functionCall assumeParamInfo indent =
211252 let indentStr = Text. replicate indent " "
212253 -- Wrap function call with LET bindings for ASSUME params
213254 wrapWithAssumes :: Text -> Text
214255 wrapWithAssumes innerExpr = foldr wrapOne innerExpr assumeParamInfo
215256 where
216- wrapOne ((name, _), isBoolean, _) expr =
217- let valueExpr = if isBoolean
218- then " (fromMaybe FALSE (args's " <> name <> " ))"
219- else " unwrapped_" <> name
220- in " LET " <> name <> " = " <> valueExpr <> " IN " <> expr
257+ wrapOne ((name, _), isBoolean, _, isDate) expr =
258+ let quotedName = quoteIdent name
259+ valueExpr = if isBoolean
260+ then " (fromMaybe FALSE (args's " <> quotedName <> " ))"
261+ else if isDate then dateConvertedVar name else unwrappedVar name
262+ in " LET " <> quotedName <> " = " <> valueExpr <> " IN " <> expr
221263 in [indentStr <> " JUST (" <> wrapWithAssumes functionCall <> " )" ]
222- generateNestedConsiderWithAssumes (((name, _), _, _): rest) functionCall assumeParamInfo indent =
264+ generateNestedConsiderWithAssumes (((name, _), _, _, isDate ): rest) functionCall assumeParamInfo indent =
223265 let indentStr = Text. replicate indent " "
224- in [ indentStr <> " CONSIDER args's " <> name
225- , indentStr <> " WHEN JUST unwrapped_" <> name <> " THEN"
226- ] ++
227- generateNestedConsiderWithAssumes rest functionCall assumeParamInfo (indent + 2 ) ++
228- [ indentStr <> " WHEN NOTHING THEN NOTHING"
229- ]
266+ quotedName = quoteIdent name
267+ in if isDate
268+ then
269+ -- For DATE types: first unwrap the MAYBE STRING, then convert to DATE via TODATE
270+ [ indentStr <> " CONSIDER args's " <> quotedName
271+ , indentStr <> " WHEN JUST " <> unwrappedVar name <> " THEN"
272+ , indentStr <> " CONSIDER TODATE " <> unwrappedVar name
273+ , indentStr <> " WHEN JUST " <> dateConvertedVar name <> " THEN"
274+ ] ++
275+ generateNestedConsiderWithAssumes rest functionCall assumeParamInfo (indent + 4 ) ++
276+ [ indentStr <> " WHEN NOTHING THEN NOTHING -- date parse failed"
277+ , indentStr <> " WHEN NOTHING THEN NOTHING"
278+ ]
279+ else
280+ -- For non-DATE types: just unwrap the MAYBE
281+ [ indentStr <> " CONSIDER args's " <> quotedName
282+ , indentStr <> " WHEN JUST " <> unwrappedVar name <> " THEN"
283+ ] ++
284+ generateNestedConsiderWithAssumes rest functionCall assumeParamInfo (indent + 2 ) ++
285+ [ indentStr <> " WHEN NOTHING THEN NOTHING"
286+ ]
287+
288+ -- | Create a variable name for date-converted values
289+ -- For names with spaces, creates a backtick-quoted identifier like `dateConverted_earn date`
290+ dateConvertedVar :: Text -> Text
291+ dateConvertedVar name
292+ | Text. any (== ' ' ) name = " `dateConverted_" <> name <> " `"
293+ | otherwise = " dateConverted_" <> name
0 commit comments