geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
mod error;

use self::error::DecodeFailure;
use super::JsonProvider;
use crate::gleam_json::GleamJsonHostProfile;
use crate::gleam_json::schema::{
    DecodeConstructions, DecodeDictIndex, DecodeDynamicIndex, DecodeErrorIndex, DecodeListIndex,
    DynamicDict, DynamicList, JsonDynamicError, JsonDynamicOk, JsonDynamicResult, UnexpectedByte,
    UnexpectedEndOfInput, UnexpectedSequence,
};
use crate::gleam_stdlib::{Dynamic, create_dynamic_dict, create_dynamic_value};
use crate::{
    BitArrayValue, HostCall, HostCallCompletion, HostCallError, HostConstructions, HostExternal,
    HostList,
};
use ecow::EcoString;
use jiter::{Jiter, Peek};
use num_bigint::BigInt;

pub(in crate::gleam_json) fn decode_to_dynamic<'call, Profile>(
    mut call: HostCall<'call, Profile, JsonProvider<Profile>, JsonDynamicResult>,
    constructions: HostConstructions<'call, DecodeConstructions>,
    json: BitArrayValue,
) -> Result<HostCallCompletion<'call, JsonDynamicResult>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    let decoded = if json.bit_len().is_multiple_of(8) {
        parse_dynamic(&mut call, &constructions, json.bytes())
    } else {
        Err(DecodeFailure::Byte(EcoString::new()))
    };

    match decoded {
        Ok(value) => Ok(call.return_custom::<JsonDynamicOk>((value, ()))),
        Err(DecodeFailure::EndOfInput) => {
            let error = call.construct_custom::<UnexpectedEndOfInput>(
                constructions.at::<DecodeErrorIndex>(),
                (),
            );
            Ok(call.return_custom::<JsonDynamicError>((error, ())))
        }
        Err(DecodeFailure::Byte(byte)) => {
            let error = call.construct_custom::<UnexpectedByte>(
                constructions.at::<DecodeErrorIndex>(),
                (byte, ()),
            );
            Ok(call.return_custom::<JsonDynamicError>((error, ())))
        }
        Err(DecodeFailure::Sequence(sequence)) => {
            let error = call.construct_custom::<UnexpectedSequence>(
                constructions.at::<DecodeErrorIndex>(),
                (sequence, ()),
            );
            Ok(call.return_custom::<JsonDynamicError>((error, ())))
        }
    }
}

enum ParseFrame<'call> {
    Array(Vec<HostExternal<'call, Dynamic>>),
    Object {
        entries: Vec<(EcoString, HostExternal<'call, Dynamic>)>,
        pending_key: EcoString,
    },
}

fn parse_dynamic<'call, Profile>(
    call: &mut HostCall<'call, Profile, JsonProvider<Profile>, JsonDynamicResult>,
    constructions: &HostConstructions<'call, DecodeConstructions>,
    input: &[u8],
) -> Result<HostExternal<'call, Dynamic>, DecodeFailure>
where
    Profile: GleamJsonHostProfile,
{
    let mut parser = Jiter::new(input);
    let mut frames = Vec::new();
    let mut next = parser
        .peek()
        .map_err(|error| DecodeFailure::from_jiter(input, error))?;

    'parse: loop {
        let mut value = if next == Peek::Null {
            parser
                .known_null()
                .map_err(|error| DecodeFailure::from_jiter(input, error))?;
            create_dynamic_value::<Profile, JsonProvider<Profile>, JsonDynamicResult, ()>(
                call,
                constructions.at::<DecodeDynamicIndex>(),
                (),
            )
        } else if matches!(next, Peek::True | Peek::False) {
            let value = parser
                .known_bool(next)
                .map_err(|error| DecodeFailure::from_jiter(input, error))?;
            create_dynamic_value::<Profile, JsonProvider<Profile>, JsonDynamicResult, bool>(
                call,
                constructions.at::<DecodeDynamicIndex>(),
                value,
            )
        } else if next == Peek::String {
            let value = parser
                .known_str()
                .map(EcoString::from)
                .map_err(|error| DecodeFailure::from_jiter(input, error))?;
            create_dynamic_value::<Profile, JsonProvider<Profile>, JsonDynamicResult, EcoString>(
                call,
                constructions.at::<DecodeDynamicIndex>(),
                value,
            )
        } else if next == Peek::Array {
            match parser
                .known_array()
                .map_err(|error| DecodeFailure::from_jiter(input, error))?
            {
                Some(first) => {
                    frames.push(ParseFrame::Array(Vec::new()));
                    next = first;
                    continue 'parse;
                }
                None => create_dynamic_list(call, constructions, Vec::new()),
            }
        } else if next == Peek::Object {
            match parser
                .known_object()
                .map(|key| key.map(EcoString::from))
                .map_err(|error| DecodeFailure::from_jiter(input, error))?
            {
                Some(pending_key) => {
                    frames.push(ParseFrame::Object {
                        entries: Vec::new(),
                        pending_key,
                    });
                    next = parser
                        .peek()
                        .map_err(|error| DecodeFailure::from_jiter(input, error))?;
                    continue 'parse;
                }
                None => create_dynamic_object(call, constructions, Vec::new()),
            }
        } else if next.is_num() {
            let number = parser
                .known_number_bytes(next)
                .map_err(|error| DecodeFailure::from_jiter(input, error))?;
            create_dynamic_number(call, constructions, number)?
        } else {
            return Err(DecodeFailure::Byte(
                format!("0x{:02X}", next.into_inner()).into(),
            ));
        };

        loop {
            value = match frames.pop() {
                None => {
                    parser
                        .finish()
                        .map_err(|error| DecodeFailure::from_jiter(input, error))?;
                    return Ok(value);
                }
                Some(ParseFrame::Array(mut values)) => {
                    values.push(value);
                    match parser
                        .array_step()
                        .map_err(|error| DecodeFailure::from_jiter(input, error))?
                    {
                        Some(peek) => {
                            frames.push(ParseFrame::Array(values));
                            next = peek;
                            continue 'parse;
                        }
                        None => create_dynamic_list(call, constructions, values),
                    }
                }
                Some(ParseFrame::Object {
                    mut entries,
                    pending_key,
                }) => {
                    entries.push((pending_key, value));
                    match parser
                        .next_key()
                        .map(|key| key.map(EcoString::from))
                        .map_err(|error| DecodeFailure::from_jiter(input, error))?
                    {
                        Some(next_key) => {
                            frames.push(ParseFrame::Object {
                                entries,
                                pending_key: next_key,
                            });
                            next = parser
                                .peek()
                                .map_err(|error| DecodeFailure::from_jiter(input, error))?;
                            continue 'parse;
                        }
                        None => create_dynamic_object(call, constructions, entries),
                    }
                }
            };
        }
    }
}

enum ParsedNumber {
    Int(BigInt),
    Float(f64),
}

fn create_dynamic_number<'call, Profile>(
    call: &mut HostCall<'call, Profile, JsonProvider<Profile>, JsonDynamicResult>,
    constructions: &HostConstructions<'call, DecodeConstructions>,
    number: &[u8],
) -> Result<HostExternal<'call, Dynamic>, DecodeFailure>
where
    Profile: GleamJsonHostProfile,
{
    match parse_number(number)? {
        ParsedNumber::Int(value) => Ok(create_dynamic_value::<
            Profile,
            JsonProvider<Profile>,
            JsonDynamicResult,
            BigInt,
        >(
            call, constructions.at::<DecodeDynamicIndex>(), value
        )),
        ParsedNumber::Float(value) => Ok(create_dynamic_value::<
            Profile,
            JsonProvider<Profile>,
            JsonDynamicResult,
            f64,
        >(
            call, constructions.at::<DecodeDynamicIndex>(), value
        )),
    }
}

fn parse_number(number: &[u8]) -> Result<ParsedNumber, DecodeFailure> {
    if !number.contains(&b'.') && !number.contains(&b'e') && !number.contains(&b'E') {
        let Some(value) = BigInt::parse_bytes(number, 10) else {
            return Err(DecodeFailure::Byte(
                number
                    .first()
                    .map_or_else(EcoString::new, |byte| format!("0x{byte:02X}").into()),
            ));
        };
        return Ok(ParsedNumber::Int(value));
    }

    let Ok(text) = std::str::from_utf8(number) else {
        return Err(DecodeFailure::Byte(EcoString::new()));
    };
    let Ok(value) = text.parse::<f64>() else {
        return Err(DecodeFailure::overflow(number));
    };
    if !value.is_finite() {
        return Err(DecodeFailure::overflow(number));
    }
    Ok(ParsedNumber::Float(value))
}

fn create_dynamic_list<'call, Profile>(
    call: &mut HostCall<'call, Profile, JsonProvider<Profile>, JsonDynamicResult>,
    constructions: &HostConstructions<'call, DecodeConstructions>,
    values: Vec<HostExternal<'call, Dynamic>>,
) -> HostExternal<'call, Dynamic>
where
    Profile: GleamJsonHostProfile,
{
    let values: HostList<'call, Dynamic> =
        call.construct_list(constructions.at::<DecodeListIndex>(), values);
    create_dynamic_value::<Profile, JsonProvider<Profile>, JsonDynamicResult, DynamicList>(
        call,
        constructions.at::<DecodeDynamicIndex>(),
        values,
    )
}

fn create_dynamic_object<'call, Profile>(
    call: &mut HostCall<'call, Profile, JsonProvider<Profile>, JsonDynamicResult>,
    constructions: &HostConstructions<'call, DecodeConstructions>,
    entries: Vec<(EcoString, HostExternal<'call, Dynamic>)>,
) -> HostExternal<'call, Dynamic>
where
    Profile: GleamJsonHostProfile,
{
    let entries = entries
        .into_iter()
        .map(|(key, value)| {
            let key = create_dynamic_value::<
                Profile,
                JsonProvider<Profile>,
                JsonDynamicResult,
                EcoString,
            >(call, constructions.at::<DecodeDynamicIndex>(), key);
            (key, value)
        })
        .collect::<Vec<_>>();
    let dict = create_dynamic_dict(call, constructions.at::<DecodeDictIndex>(), entries);
    create_dynamic_value::<Profile, JsonProvider<Profile>, JsonDynamicResult, DynamicDict>(
        call,
        constructions.at::<DecodeDynamicIndex>(),
        dict,
    )
}

#[cfg(test)]
mod tests {
    use super::{DecodeFailure, ParsedNumber, parse_number};

    #[test]
    fn parses_validated_number_tokens_and_maps_defensive_failures() {
        assert!(
            matches!(parse_number(b"-123"), Ok(ParsedNumber::Int(value)) if value == (-123).into())
        );
        assert!(matches!(
            parse_number(b"1.25"),
            Ok(ParsedNumber::Float(1.25))
        ));
        assert!(matches!(parse_number(b""), Err(DecodeFailure::Byte(byte)) if byte.is_empty()));
        assert!(matches!(parse_number(b"x"), Err(DecodeFailure::Byte(byte)) if byte == "0x78"));
        assert!(
            matches!(parse_number(b"\xFF."), Err(DecodeFailure::Byte(byte)) if byte.is_empty())
        );
        assert!(
            matches!(parse_number(b"x."), Err(DecodeFailure::Sequence(sequence)) if sequence == "x.")
        );
        assert!(
            matches!(parse_number(b"1e400"), Err(DecodeFailure::Sequence(sequence)) if sequence == "1.0e400")
        );
    }
}