//// Decoder module for GJ //// //// See module gj for the public interface import gleam/bit_builder.{BitBuilder} import gleam/bit_string import gleam/float import gleam/int import gleam/io import gleam/list import gleam/result import gleam/string import generic_json.{Array, Boolean, JSON, Null, Number, Object, String} /// Types of errors that can occur during parsing pub type ParseError { UnexpectedToken(String) UnexpectedEndOfInput InvalidString InvalidUnicodeSequence InvalidNumber } /// Location of a parser error pub type ParseErrorLocation { ParseErrorLocation(row: Int, column: Int) } type State { State(column: Int, row: Int, rest: String) } pub fn decode( str: String, ) -> Result(JSON, tuple(ParseError, ParseErrorLocation)) { let state0 = State(column: 0, row: 0, rest: str) let parse_result = parse_value(state0) let final = case parse_result { tuple(state1, Ok(val)) -> { let state2 = chomp_whitespace(state1) case next_grapheme(state2.rest) { Error(Nil) -> tuple(state2, Ok(val)) Ok(tuple(c, _)) -> tuple(state2, Error(UnexpectedToken(c))) } } _ -> parse_result } case final { tuple(_, Ok(v)) -> Ok(v) tuple(s, Error(pe)) -> Error(tuple(pe, ParseErrorLocation(row: s.row, column: s.column))) } } fn parse_value(state0: State) -> tuple(State, Result(JSON, ParseError)) { let state = chomp_whitespace(state0) let parse_result = case next_grapheme(state.rest) { Error(Nil) -> tuple(state, Error(UnexpectedEndOfInput)) Ok(tuple(c, rest)) -> case c { // null "n" -> case pop_str(rest, 3) { Ok(tuple(["u", "l", "l"], rest1)) -> tuple( State(..state, column: state.column + 4, rest: rest1), Ok(Null), ) _ -> tuple( State(..state, column: state.column + 1, rest: rest), Error(UnexpectedToken(c)), ) } // true "t" -> case pop_str(rest, 3) { Ok(tuple(["r", "u", "e"], rest1)) -> tuple( State(..state, column: state.column + 4, rest: rest1), Ok(Boolean(True)), ) _ -> tuple( State(..state, column: state.column + 1, rest: rest), Error(UnexpectedToken(c)), ) } // false "f" -> case pop_str(rest, 4) { Ok(tuple(["a", "l", "s", "e"], rest1)) -> tuple( State(..state, column: state.column + 5, rest: rest1), Ok(Boolean(False)), ) _ -> tuple( State(..state, column: state.column + 1, rest: rest), Error(UnexpectedToken(c)), ) } // array start "[" -> parse_array( State(..state, column: state.column + 1, rest: rest), AValueOrEnd, [], ) // object start "{" -> parse_object( State(..state, column: state.column + 1, rest: rest), OKeyOrEnd, [], ) // string start "\"" -> State(..state, column: state.column + 1, rest: rest) |> parse_string() // number start "-" -> State(..state, column: state.column + 1, rest: rest) |> parse_number(LeadingMinus, bit_builder.from_string(c)) "0" -> State(..state, column: state.column + 1, rest: rest) |> parse_number(LeadingZero, bit_builder.from_string(c)) "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> State(..state, column: state.column + 1, rest: rest) |> parse_number(Whole, bit_builder.from_string(c)) // unexpected char _ -> tuple(state, Error(UnexpectedToken(c))) } } parse_result } type NumberPhase { LeadingMinus LeadingZero Whole DecimalInit Decimal ExponentInit ExponentLeadingSign DigitOrEnd NumberError } fn parse_number( state0: State, phase: NumberPhase, acc: BitBuilder, ) -> tuple(State, Result(JSON, ParseError)) { let can_end = case phase { LeadingZero | Whole | Decimal | DigitOrEnd -> True LeadingMinus | DecimalInit | ExponentInit | NumberError | ExponentLeadingSign -> False } case next_grapheme(state0.rest) { Error(Nil) -> case can_end { True -> case resolve_number(acc) { Ok(json) -> tuple(state0, Ok(json)) Error(_) -> tuple(state0, Error(InvalidNumber)) } False -> tuple(state0, Error(UnexpectedEndOfInput)) } Ok(tuple(c, rest)) -> case c { " " | "\n" | "]" | "}" | "," | "\t" -> case can_end { True -> case resolve_number(acc) { Ok(json) -> tuple(state0, Ok(json)) Error(_) -> tuple(state0, Error(InvalidNumber)) } False -> tuple(state0, Error(UnexpectedEndOfInput)) } _ -> { let new_phase = case phase { LeadingMinus -> case c { "0" -> LeadingZero "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> Whole _ -> NumberError } LeadingZero -> case c { "." -> DecimalInit "e" | "E" -> ExponentInit _ -> NumberError } Whole -> case c { "." -> DecimalInit "e" | "E" -> ExponentInit "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> Whole _ -> NumberError } DecimalInit -> case c { "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> Decimal _ -> NumberError } Decimal -> case c { "e" | "E" -> ExponentInit "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> Decimal _ -> NumberError } ExponentInit -> case c { "+" | "-" -> ExponentLeadingSign "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> DigitOrEnd _ -> NumberError } ExponentLeadingSign | DigitOrEnd -> case c { "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> DigitOrEnd _ -> NumberError } NumberError -> NumberError } case new_phase { NumberError -> tuple(state0, Error(UnexpectedToken(c))) _ -> { // These coersions are to play nice with Gleam's int.parse and float.parse let new_acc = case phase, new_phase, c { LeadingZero, ExponentInit, _ -> acc |> bit_builder.append_string(".0") |> bit_builder.append_string(c) Whole, ExponentInit, _ -> acc |> bit_builder.append_string(".0") |> bit_builder.append_string(c) ExponentInit, ExponentLeadingSign, "+" -> acc _, _, _ -> acc |> bit_builder.append_string(c) } parse_number( State(..state0, column: state0.column + 1, rest: rest), new_phase, new_acc, ) } } } } } } type ObjectSearch { OKeyOrEnd OKey OColon(String) OValue(String) OCommaOrEnd } fn parse_object( state0: State, search: ObjectSearch, stack: List(tuple(String, JSON)), ) -> tuple(State, Result(JSON, ParseError)) { let state = chomp_whitespace(state0) // TODO: can you have multiple guards? let can_parse_string = case search { OKey | OKeyOrEnd -> True OColon(_) | OValue(_) | OCommaOrEnd -> False } case next_grapheme(state.rest) { Error(Nil) -> tuple(state, Error(UnexpectedEndOfInput)) Ok(tuple(c, rest)) -> case c { "," -> case search { OCommaOrEnd -> State(..state, column: state.column + 1, rest: rest) |> parse_object(OKey, stack) OColon(_) | OValue(_) | OKeyOrEnd | OKey -> tuple( state, Error(UnexpectedToken(c)), ) } "}" -> case search { OCommaOrEnd | OKeyOrEnd -> tuple( State(..state, column: state.column + 1, rest: rest), Ok(Object(list.reverse(stack))), ) OValue(_) | OColon(_) | OKey -> tuple( State(..state, column: state.column + 1, rest: rest), Error(UnexpectedToken(c)), ) } ":" -> case search { OColon(str) -> State(..state, column: state.column + 1, rest: rest) |> parse_object(OValue(str), stack) OValue(_) | OCommaOrEnd | OKeyOrEnd | OKey -> tuple( State(..state, column: state.column + 1, rest: rest), Error(UnexpectedToken(c)), ) } "\"" if can_parse_string -> { let state1 = State(..state, column: state.column + 1, rest: rest) case parse_string(state1) { tuple(state2, Ok(String(str))) -> parse_object(state2, OColon(str), stack) err -> err } } _ -> case search { OValue(key) -> case parse_value(state) { tuple(state2, Ok(json)) -> parse_object(state2, OCommaOrEnd, [tuple(key, json), ..stack]) err -> err } OColon(_) | OKey | OCommaOrEnd | OKeyOrEnd -> tuple( State(..state, column: state.column + 1, rest: rest), Error(UnexpectedToken(c)), ) } } } } type CharStatus { Allowed(String) NotAllowed } fn char_status(c: String) -> CharStatus { case valid_in_string(c) { False -> NotAllowed True -> Allowed(c) } } fn parse_string(state0: State) -> tuple(State, Result(JSON, ParseError)) { do_parse_string(state0, bit_builder.from_string("")) } type EscapeResult { Add(String) InvalidEscape Unicode(tuple(State, Result(String, ParseError))) } fn do_parse_string( state0: State, acc: BitBuilder, ) -> tuple(State, Result(JSON, ParseError)) { case next_grapheme(state0.rest) { Error(Nil) -> tuple(state0, Error(UnexpectedEndOfInput)) Ok(tuple(c, rest)) -> case c { "\\" -> case next_grapheme(rest) { Error(Nil) -> tuple(state0, Error(UnexpectedEndOfInput)) Ok(tuple(c2, rest2)) -> { let next = case c2 { "\"" -> Add("\"") "/" -> Add("/") "\\" -> Add("\\") "b" -> Add("\b") "f" -> Add("\f") "n" -> Add("\n") "r" -> Add("\r") "t" -> Add("\t") "u" -> State(..state0, column: state0.column + 2, rest: rest2) |> parse_unicode() |> Unicode() _ -> InvalidEscape } case next { Add(n) -> State(..state0, column: state0.column + 2, rest: rest2) |> do_parse_string(bit_builder.append_string(acc, n)) InvalidEscape -> tuple(state0, Error(UnexpectedToken(c2))) Unicode(tuple(state2, uni)) -> case uni { Ok(str) -> state2 |> do_parse_string(bit_builder.append_string(acc, str)) Error(e) -> tuple(state2, Error(e)) } } } } _ -> case char_status(c) { Allowed("\"") -> case bit_string.to_string(bit_builder.to_bit_string(acc)) { Ok(str) -> tuple( State(..state0, column: state0.column + 1, rest: rest), Ok(String(str)), ) _ -> tuple(state0, Error(InvalidString)) } Allowed(_) -> State(..state0, column: state0.column + 1, rest: rest) |> do_parse_string(bit_builder.append_string(acc, c)) NotAllowed -> tuple(state0, Error(UnexpectedToken(c))) } } } } type UnicodeToBinResult { Incomplete Nogood Complete(String) } fn parse_unicode(state0: State) -> tuple(State, Result(String, ParseError)) { case pop_str(state0.rest, 4) { Ok(tuple([a, b, c, d], rest)) -> case hex_to_unicode(a, b, c, d) { Ok(uni) -> case uni_to_bin([uni]) { Incomplete -> { let state = State(..state0, column: state0.column + 4, rest: rest) case pop_str(state.rest, 6) { Ok(tuple(["\\", "u", e, f, g, h], rest2)) -> case hex_to_unicode(e, f, g, h) { Ok(uni2) -> case uni_to_bin([uni, uni2]) { Complete(str) -> tuple( State(..state, column: state.column + 4, rest: rest2), Ok(str), ) _ -> tuple(state, Error(InvalidUnicodeSequence)) } Error(int) -> tuple( State(..state, column: state.column + int, rest: rest), Error(UnexpectedToken( list.at([e, f, g, h], int - 1) |> result.unwrap(""), )), ) } _ -> tuple(state, Error(UnexpectedEndOfInput)) } } Nogood -> tuple(state0, Error(InvalidUnicodeSequence)) Complete(str) -> tuple( State(..state0, column: state0.column + 4, rest: rest), Ok(str), ) } Error(int) -> tuple( State(..state0, column: state0.column + int, rest: rest), Error(UnexpectedToken( list.at([a, b, c, d], int - 1) |> result.unwrap(""), )), ) } _ -> tuple(state0, Error(UnexpectedEndOfInput)) } } type ArraySearch { AValueOrEnd AValue ACommaOrEnd } fn parse_array( state0: State, search: ArraySearch, stack: List(JSON), ) -> tuple(State, Result(JSON, ParseError)) { let state = chomp_whitespace(state0) case next_grapheme(state.rest) { Error(Nil) -> tuple(state, Error(UnexpectedEndOfInput)) Ok(tuple(c, rest)) -> case c { "," -> case search { ACommaOrEnd -> parse_array( State(..state, column: state.column + 1, rest: rest), AValue, stack, ) AValueOrEnd | AValue -> tuple(state, Error(UnexpectedToken(c))) } "]" -> case search { ACommaOrEnd | AValueOrEnd -> tuple( State(..state, column: state.column + 1, rest: rest), Ok(Array(list.reverse(stack))), ) AValue -> tuple( State(..state, column: state.column + 1, rest: rest), Error(UnexpectedToken(c)), ) } _ -> case search { AValue | AValueOrEnd -> case parse_value(state) { tuple(state1, Ok(val)) -> parse_array(state1, ACommaOrEnd, [val, ..stack]) e -> e } ACommaOrEnd -> tuple( State(..state, column: state.column + 1, rest: rest), Error(UnexpectedToken(c)), ) } } } } fn chomp_whitespace(state0: State) -> State { case next_grapheme(state0.rest) { Ok(tuple(c, rest)) -> case c { " " | "\t" -> State(..state0, column: state0.column + 1, rest: rest) |> chomp_whitespace() "\n" -> State(column: 0, row: state0.row + 1, rest: rest) |> chomp_whitespace() _ -> state0 } Error(Nil) -> state0 } } // Get more than 1 grapheme of the head of a string fn pop_str(rest: String, n: Int) -> Result(tuple(List(String), String), Nil) { pop_str_help(n, [], rest) } fn pop_str_help( n: Int, acc: List(String), rest: String, ) -> Result(tuple(List(String), String), Nil) { case next_grapheme(rest) { Ok(tuple(c, rest)) -> case n { 1 -> Ok(tuple(list.reverse([c, ..acc]), rest)) _ -> pop_str_help(n - 1, [c, ..acc], rest) } _ -> Error(Nil) } } fn hex_to_int(c: String) -> Result(Int, Nil) { let i = case c { "0" -> 0 "1" -> 1 "2" -> 2 "3" -> 3 "4" -> 4 "5" -> 5 "6" -> 6 "7" -> 7 "8" -> 8 "9" -> 9 "a" | "A" -> 10 "b" | "B" -> 11 "c" | "C" -> 12 "d" | "D" -> 13 "e" | "E" -> 14 "f" | "F" -> 15 _ -> 16 } case i { 16 -> Error(Nil) x -> Ok(x) } } fn hex_to_unicode( a: String, b: String, c: String, d: String, ) -> Result(BitString, Int) { case hex_to_int(a) { Error(_) -> Error(1) Ok(aa) -> case hex_to_int(b) { Error(_) -> Error(2) Ok(bb) -> case hex_to_int(c) { Error(_) -> Error(3) Ok(cc) -> case hex_to_int(d) { Error(_) -> Error(4) Ok(dd) -> { assert <> = <> Ok(<>) } } } } } } fn resolve_number(bb: BitBuilder) -> Result(JSON, Nil) { let str = bb |> bit_builder.to_bit_string() |> bit_string.to_string() |> result.unwrap("") case float.parse(str) { Ok(f) -> Ok(Number(f)) Error(_) -> case int.parse(str) { Ok(i) -> Ok(Number(int.to_float(i))) Error(_) -> Error(Nil) } } } // TODO: Something is broken with string.pop_grapheme // figure it out and provide a patch? fn next_grapheme(str: String) -> Result(tuple(String, String), Nil) { // string.pop_grapheme(str) erl_next_grapheme(str) } external fn erl_next_grapheme(str: String) -> Result(tuple(String, String), Nil) = "gj_bridge" "next_grapheme" external fn uni_to_bin(uni: List(BitString)) -> UnicodeToBinResult = "gj_bridge" "uni_to_bin" external fn valid_in_string(in: String) -> Bool = "gj_bridge" "valid_in_string"