diff --git a/lib/Base/Char.fram b/lib/Base/Char.fram index 956279fe..4ca8bd00 100644 --- a/lib/Base/Char.fram +++ b/lib/Base/Char.fram @@ -14,3 +14,5 @@ pub method ge = (extern dbl_geInt : Char -> Char -> Bool) pub method le = (extern dbl_leInt : Char -> Char -> Bool) pub method toString = (extern dbl_chrToString : Char -> String) + +pub method unescaped = (extern dbl_unescapedChrToString : Char -> String) diff --git a/lib/Format.fram b/lib/Format.fram new file mode 100644 index 00000000..c7dd20ea --- /dev/null +++ b/lib/Format.fram @@ -0,0 +1,482 @@ +import /List + +## # Format methods + +{## + This module provides more advanced format methods. + + Each type has an associated data type that carries additional formatting + options. Those structures may contain a nested format for subtypes. + + Default formatting should produce syntactically correct Fram expression. + This constraint requires format methods to respect the operator and function + application precedence and insert parenthesis if needed. It is handled by + passing a precedence index of parent node. Application precedence strength + defaults to 10. Parentheses might be disabled. + ##} + +# Handles parenthesis insertion +let addParenths + (str : String) + (precedence : Int) + (parent : Option Int) + (addParents : Option Bool) = + if addParents.unwrapOr True then + (let parent = parent.unwrapOr 0 in + if precedence <= parent then + "(" + str + ")" + else + str) + else + str + +# Handles padding options +let padding + (str : String) + (size : Option Int) + (fill : Option Char) + (alignLeft : Option Bool) = + + let size = size.unwrapOr 0 + let fill = fill.unwrapOr ' ' + let alignLeft = alignLeft.unwrapOr False + + let _ = assert {msg="Negative padding"} (size >= 0) + + let df = size - str.length + let df = if df < 0 then 0 else df + let rec padding df = + if 0 == df then + [] + else + fill :: padding (df - 1) + let padding = charListToStr (padding df) in + if alignLeft then + str + padding + else + padding + str + +parameter X +parameter XFmt +parameter method format : {?fmt : XFmt, ?prec : Int} -> X ->> String + +parameter Y +parameter YFmt +parameter method format : {?fmt : YFmt, ?prec : Int} -> Y ->> String + +## ## Unit + +{## Unit format options ##} +pub data UnitFmt = + {## + Unit format options + + @param ?unit Overrides constructor name. + @param ?size Total size to occupy. + @param ?fill Padding fill character. + @param ?alignLeft Padding direction. + ##} + UnitFmt of { ?unit : String + , ?size : Int + , ?fill : Char + , ?alignLeft : Bool } + +{## + Unit format method. + + @param ?prec Parent precedence. + @param ?fmt Format options. + ##} +pub method format {?prec : Int, ?fmt : UnitFmt} () = + let UnitFmt {unit, size, fill, alignLeft} = fmt.unwrapOr UnitFmt in + let ctor = unit.unwrapOr "()" in + padding ctor size fill alignLeft + +## # Int + +{## Int format options ##} +pub data IntFmt = + {## + Int format options. + + @param ?base Number base. Function accepts bases from 2 to 36. + @param ?prefix Prefix, by default empty. + @param ?size Total size. + @param ?fill Padding fill charcter. + @param ?alignLeft Padding direction. + @param ?parenths Inserts parenthesis when minus sign is added. + ##} + IntFmt of { ?base : Int + , ?prefix : String + , ?size : Int + , ?fill : Char + , ?alignLeft : Bool + , ?parenths : Bool } + +pub method format {?prec : Int, ?fmt : IntFmt} (self : Int) = + let IntFmt {base, prefix, parenths} = fmt.unwrapOr IntFmt + let IntFmt {size, fill, alignLeft} = fmt.unwrapOr IntFmt + let base = base.unwrapOr 10 + let prefix = prefix.unwrapOr "" + let _ = assert {msg="Int base out of bounds"} (2 <= base && base <= 36) + # handles negative number in base 10 + let (self, prefix, neg) = + if base == 10 && self < 0 then + (-self, prefix + "-", True) + else + (self, prefix, False) + # constructs representation + let rec iter (n : Int) acc = + if n == 0 then + if List.isEmpty acc then ['0'] else acc + else + (let digit = n % base + let shift = n / base + let ~onError = fn _ => ' ' + let c = + if digit <= 9 then + chr ('0'.code + digit) + else + chr ('A'.code + digit - 10) + in iter shift (c :: acc)) + # final result + let num = prefix + charListToStr (iter self []) + let parenths = Some (neg && (parenths.unwrapOr True)) + let num = addParenths num 150 prec parenths in + padding num size fill alignLeft + +## ## Char + +{## Char format options. ##} +pub data CharFmt = + {## + Char format options. + + @param ?escape Escapes Chars. Defaults to True. + @param ?asInt Converts char to int and formats as such. + @param ?size Total size. + @param ?fill Padding fill size. + @param ?alignLeft Padding direction. + ##} + CharFmt of { ?escape : Bool + , ?asInt : IntFmt + , ?size : Int + , ?fill : Char + , ?alignLeft : Bool } + +let specialReprs = + [ ('\"', "\\\"") + , ('\'', "'" ) + , ('\0', "\\0") + , ('\n', "\\n") + , ('\b', "\\b") + , ('\t', "\\t") + , ('\r', "\\r") + , ('\v', "\\v") + , ('\a', "\\a") + , ('\f', "\\f") + ] + +## Converts a character to it's representation withous enquoting. +pub let charRepr (x : Char) = + let matchEsc (chr, repr) = + if x == chr then Some repr else None in + match List.findMap matchEsc specialReprs with + | Some c => c + | None => + let code = x.code in + if 32 <= code && code <= 127 then + x.toString + else + code.format {fmt=IntFmt {base=16, prefix="\\x"}} + end + +{## + Char format method. + + @param ?prec Parent precedence. + @param ?fmt Format arguments. + ##} +pub method format {?prec : Int, ?fmt : CharFmt} (self : Char) = + let CharFmt {escape, asInt} = fmt.unwrapOr CharFmt + let CharFmt {size, fill, alignLeft} = fmt.unwrapOr CharFmt in + match asInt with + | Some fmt => self.code.format {prec=prec.unwrapOr 0, fmt} + | None => + let res = + if escape.unwrapOr True == False then + self.unescaped + else + (let escape = charRepr self in + "'" + escape + "'") + in padding res size fill alignLeft + end + +## ## String + +{## String format options ##} +pub data StringFmt = + {## + String format options. + + @param ?escape Enables string escaping. On by default. + @param ?size Total size. + @param ?fill Padding fill character. + @param ?alignLeft Padding direction. + ##} + StringFmt of { ?escape : Bool + , ?size : Int + , ?fill : Char + , ?alignLeft : Bool } + +{## + String format method. + + @param ?prec Parent precedence. + @param ?fmt Format options. + ##} +pub method format {?prec : Int, ?fmt : StringFmt} (self : String) = + let StringFmt {escape, size, fill, alignLeft} = fmt.unwrapOr StringFmt in + if escape.unwrapOr True == False then + padding self size fill alignLeft + else + (let xs = self.toList.map charRepr + let res = "\"" + strListCat xs + "\"" in + padding res size fill alignLeft) + +## ## Bool + +{## Bool format options. ##} +pub data BoolFmt = + {## + Bool format options + + @param ?true Overrides True constructor. + @param ?false Overrides False constructor. + @param ?size Total size. + @param ?fill Padding fill character. + @param ?alignLeft Padding direction. + ##} + BoolFmt of { ?true : String + , ?false : String + , ?size : Int + , ?fill : Char + , ?alignLeft : Bool } + +{## + Bool format method. + + @param ?prec Parend precedence. + @param ?fmt Bool format options. + ##} +pub method format {?prec : Int, ?fmt : BoolFmt} (self : Bool) = + let BoolFmt {true, false} = fmt.unwrapOr BoolFmt + let BoolFmt {size, fill, alignLeft} = fmt.unwrapOr BoolFmt + let ctor = + if self then + true.unwrapOr "True" + else + false.unwrapOr "False" in + padding ctor size fill alignLeft + +## ## List + +pub data ListFmt A = + {## + Bool format options + + @param ?opn Overrides opening bracket. + @param ?sep Overrides separator. + @param ?cls Overrides closing bracket. + @param ?inner Inner values' format. + @param ?false Overrides False constructor. + @param ?size Total size. + @param ?fill Padding fill character. + @param ?alignLeft Padding direction. + ##} + ListFmt of { ?opn : String + , ?sep : String + , ?cls : String + , ?inner : A + , ?size : Int + , ?fill : Char + , ?alignLeft : Bool } + +{## + List format method. + + @param ?prec Parend precedence. + @param ?fmt Bool format options. + ##} +pub method format + {?prec : Int, ?fmt : ListFmt XFmt } (self : List X) = + let ListFmt {opn, sep, cls, inner} = fmt.unwrapOr ListFmt + let ListFmt {size, fill, alignLeft} = fmt.unwrapOr ListFmt + let opn = opn.unwrapOr "[" + let sep = sep.unwrapOr ", " + let cls = cls.unwrapOr "]" + # handles application of optional format param + let appInner (x : X) = + match inner with + | Some fmt => x.format {fmt} + | None => x.format + end + let rec iter (xs : List X) = + match xs with + | [] => cls + | [x] => appInner x + cls + | x :: xs => appInner x + sep + iter xs + end in + let xs = opn + iter self in + padding xs size fill alignLeft + +## ## Option + +{## Option format options. ##} +pub data OptionFmt A = + {## + Option format options. + + @param ?some Overrides Some constructor. + @param ?none Overrides None constructor. + @param ?inner Inner value's format. + @param ?size Total size. + @param ?fill Padding fill character. + @param ?alignLeft Padding direction. + @param ?parenths Additional parenthesis. + ##} + OptionFmt of { ?some : String + , ?none : String + , ?inner : A + , ?size : Int + , ?fill : Char + , ?alignLeft : Bool + , ?parenths : Bool } + +{## + Option format method. + + @param ?prec Parend precedence. + @param ?fmt Bool format options. + ##} +pub method format + {?prec : Int, ?fmt : OptionFmt XFmt } (self : Option X) = + let OptionFmt {some, none, inner, parenths} = fmt.unwrapOr OptionFmt + let OptionFmt {size, fill, alignLeft} = fmt.unwrapOr OptionFmt + let appInner (x : X) = + match inner with + | Some fmt => x.format {prec=200, fmt} + | None => x.format {prec=200} + end + let str = + match self with + | None => none.unwrapOr "None" + | Some x => + let str = some.unwrapOr "Some " + appInner x in + addParenths str 200 prec parenths + end in + padding str size fill alignLeft + +## ## Pair + +{## Pair format options. ##} +pub data PairFmt A B = + {## + Pair format options. + + @param ?opn Overrides opening parenth. + @param ?sep Overrides separator. + @param ?cls Overrides closing bracket. + @param ?size Total size. + @param ?fst Format for first value. + @param ?snd Format for second value. + @param ?fill Padding fill character. + @param ?alignLeft Padding direction. + ##} + PairFmt of { ?opn : String + , ?sep : String + , ?cls : String + , ?fst : A + , ?snd : B + , ?size : Int + , ?fill : Char + , ?alignLeft : Bool } + +{## + Pair format method. + + @param ?prec Parend precedence. + @param ?fmt Bool format options. + ##} +pub method format {?fmt : PairFmt XFmt YFmt, ?prec : Int } ((v1, v2) : Pair X Y) = + let PairFmt {opn, sep, cls} = fmt.unwrapOr PairFmt + let PairFmt {fst, snd} = fmt.unwrapOr PairFmt + let PairFmt {size, fill, alignLeft} = fmt.unwrapOr PairFmt + + let fst = match fst with + | Some fmt => v1.format {fmt} + | None => v1.format + end + let snd = match snd with + | Some fmt => v2.format {fmt} + | None => v2.format + end + + let opn = opn.unwrapOr "(" + let sep = sep.unwrapOr ", " + let cls = cls.unwrapOr ")" + let res = opn + fst + sep + snd + cls in + padding res size fill alignLeft + +{# Either #} + +{## Either format options. ##} +pub data EitherFmt A B = + {## + Either format options. + + @param ?left Overrides Left constructor. + @param ?right Overrides Right constructor. + @param ?innerL Format for left value. + @param ?innerR Format for right value. + @param ?size Total size. + @param ?fill Padding fill character. + @param ?alignLeft Padding direction. + @param ?parenths Additional parenthesis. + ##} + EitherFmt of { ?left : String + , ?right : String + , ?innerL : A + , ?innerR : B + , ?size : Int + , ?fill : Char + , ?alignLeft : Bool + , ?parenths : Bool } + +{## + Either format method. + + @param ?prec Parend precedence. + @param ?fmt Bool format options. + ##} +pub method format {?fmt : EitherFmt XFmt YFmt, ?prec : Int } (self : Either X Y) = + let EitherFmt {size, fill, alignLeft, parenths} = fmt.unwrapOr EitherFmt + let res = + match self with + | Left x => + let EitherFmt {left, innerL} = fmt.unwrapOr EitherFmt + let left = left.unwrapOr "Left " in + match innerL with + | Some fmt => left + x.format {prec=200, fmt} + | None => left + x.format {prec=200} + end + | Right y => + let EitherFmt {right, innerR} = fmt.unwrapOr EitherFmt + let right = right.unwrapOr "Right " in + match innerR with + | Some fmt => right + y.format {prec=200, fmt} + | None => right + y.format {prec=200} + end + end + let res = addParenths res 200 prec parenths in + padding res size fill alignLeft diff --git a/lib/Prelude.fram b/lib/Prelude.fram index 4e3ad914..7edec800 100644 --- a/lib/Prelude.fram +++ b/lib/Prelude.fram @@ -35,6 +35,8 @@ pub let chr {~onError : Unit ->[_] Char} (n : Int) = else ~onError () +pub let strListCat = (extern dbl_strListCat : List String -> String) + pub let printStrLn = extern dbl_printStrLn : String ->[IO] Unit pub let printStr = extern dbl_printStr : String ->[IO] Unit pub let printInt = extern dbl_printInt : Int ->[IO] Unit diff --git a/src/Eval/External.ml b/src/Eval/External.ml index 529faa19..ad5fb514 100644 --- a/src/Eval/External.ml +++ b/src/Eval/External.ml @@ -127,6 +127,7 @@ let extern_map = "dbl_strLen", str_fun (fun s -> VNum (String.length s)); "dbl_strGet", str_fun (fun s -> int_fun (fun n -> VNum (Char.code s.[n]))); "dbl_chrToString", int_fun (fun c -> VStr (Char.escaped (Char.chr c))); + "dbl_unescapedChrToString", int_fun (fun c -> VStr (String.make 1 (Char.chr c))); "dbl_chrListToStr", list_fun to_char (fun xs -> VStr (List.to_seq xs |> String.of_seq)); "dbl_chrCode", int_fun (fun c -> VNum c); diff --git a/test/stdlib/stdlib0003_Format.fram b/test/stdlib/stdlib0003_Format.fram new file mode 100644 index 00000000..835b0b85 --- /dev/null +++ b/test/stdlib/stdlib0003_Format.fram @@ -0,0 +1,100 @@ +import open Format + +# Unit +let _ = + assert (().format == "()"); + let fmt = UnitFmt {unit="Unit"} in + assert (().format {fmt} == "Unit"); + let fmt = UnitFmt {size=5, fill='#', alignLeft=True} in + assert (().format {fmt} == "()###"); + let fmt = UnitFmt {size=4, fill='?', unit="U"} in + assert (().format {fmt} == "???U") + +# Bool +let _ = + assert ((True).format == "True"); + assert ((False).format == "False"); + let fmt = BoolFmt {true="#t", false="#f", size=3} in + assert ((True).format {fmt} == " #t"); + assert ((False).format {fmt} == " #f") + +# Int +let _ = + assert (10.format == "10"); + assert ((-10).format == "-10"); + assert ((-10).format {prec=200} == "(-10)"); + assert ((-10).format {prec=200, fmt=IntFmt{parenths=False}} == "-10"); + let (num : Int) = ((15 * 36 + 27) * 36 + 10) * 36 + 22 + let fmt = IntFmt {prefix="36x", base=36} in + assert (num.format {fmt} == "36xFRAM") + +# Char +let _ = + assert ('a'.format == "'a'"); + assert ('\xFF'.format == "'\\xFF'"); + let fmt = CharFmt {escape=False, size=3} in + assert ('\n'.format {fmt} == " \n"); + let fmt = CharFmt {asInt=IntFmt {base=16, prefix="\\x"}} in + assert (' '.format {fmt} == "\\x20") + +# String +let _ = + assert ("abc".format == "\"abc\""); + assert ("\n\t\r".format == "\"\\n\\t\\r\""); + let fmt = StringFmt {escape=False} in + assert ("\n\t\r".format {fmt} == "\n\t\r") + +# List +let _ = + assert ([True, False].format == "[True, False]"); + let fmt = ListFmt + { opn="[| ", cls=" |]", sep=" " + , inner = BoolFmt {true="1", false="0"}} in + assert ([True, False].format {fmt} == "[| 1 0 |]") + +# Option +let _ = + assert ((Some ()).format == "Some ()"); + assert ((None : Option Unit).format == "None"); + let fmt = OptionFmt + { some = "S", none = "N" + , inner = UnitFmt {unit = "U"}} in + assert ((Some ()).format {fmt} == "SU") + +# Either +let _ = + assert ((Left 10 : Either Int String).format == "Left 10"); + assert ((Right "a" : Either Int String).format == "Right \"a\""); + let fmt = EitherFmt + { left="L ", right="R " + , innerL=IntFmt {prefix="(dec)"} + , innerR=StringFmt {escape=False} } in + assert ((Left 10 : Either Int String).format {fmt} == "L (dec)10"); + assert ((Right "a" : Either Int String).format {fmt} == "R a") + +# Pair +let _ = + assert (((), 'a').format == "((), 'a')"); + let fmt = PairFmt + { opn="<|", sep="+", cls="|>" + , fst=UnitFmt{unit="UNIT"} + , snd=CharFmt{escape=False} } in + assert (((), 'a').format {fmt} == "<|UNIT+a|>") + +# Everything at once +let _ = + let d = ("abc", [Left 10, Right (Some True), Right None]) + let fmt = PairFmt + { opn="{", cls="}" + , fst=StringFmt{escape=False} + , snd=ListFmt + { opn="[|", cls="|]" + , inner=EitherFmt + { left="L", right="R" + , innerL=IntFmt {base=2} + , innerR=OptionFmt + { none="N", some="S" + , parenths=False + , inner=BoolFmt {true="T", false="F"}}}}} in + assert (d.format == "(\"abc\", [Left 10, Right (Some True), Right None])"); + assert (d.format {fmt} == "{abc, [|L1010, RST, RN|]}")