untwine icon indicating copy to clipboard operation
untwine copied to clipboard

[Bug] Mark a error as non-recoverable?

Open WinnetDev opened this issue 6 months ago • 3 comments

I need to mark a error as non-recoverable. In order to make the error message clearer.

use std::{num::{ParseFloatError, ParseIntError}, ops::Range};
use untwine::{parser, parser_repl, prelude::Recoverable};

#[derive(Debug, Default)]
pub enum UnitOfMeasurement {
    #[default]
    INCHES,
    CENTIMETERS,
    DOTS,
    XDOTS
}

#[derive(Debug, Default)]
pub struct ValueWithUnit(f32, UnitOfMeasurement);

#[derive(Debug)]
pub enum GridUnit {
    FormatId(String),
    Value(ValueWithUnit)
}

/// (y, x)
pub type GridOrigin = (ValueWithUnit, ValueWithUnit);

#[derive(Debug, Recoverable)]
pub enum Command {
    Grid(GridUnit, GridOrigin),
    End,
    #[recover]
    Error(Range<usize>),
}

parser! {
    [error = ParseFormError, recover = true]
    space = #{char::is_ascii_whitespace};
    semicolon = space* ';';

    digit = '0'-'9' -> char;
    float: num=<"-"? digit+ ("." digit+)?> -> f32 { num.parse()? }

    ident_char = ["0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy"] -> char;
    ident: chars=<#[repeat(1..=6)]ident_char> -> String { chars.to_string() }

    unit_inches: ("INCHES" | "INCH" | "IN") -> UnitOfMeasurement { UnitOfMeasurement::INCHES }
    unit_centimeters: ("CM" | "CENTIMETERS") -> UnitOfMeasurement { UnitOfMeasurement::CENTIMETERS }
    unit_dots: "DOTS" -> UnitOfMeasurement { UnitOfMeasurement::DOTS }
    unit_xdots: "XDOTS" -> UnitOfMeasurement { UnitOfMeasurement::XDOTS }
    unit = (unit_inches | unit_centimeters | unit_dots | unit_xdots) -> UnitOfMeasurement;

    value_with_opt_unit: value=float space+ unit=(space+ unit)? -> ValueWithUnit { ValueWithUnit(value, unit.unwrap_or_default()) }

    omitted_value_with_dots: unit_dots -> ValueWithUnit { ValueWithUnit (1.0f32, UnitOfMeasurement::DOTS)}

    opt_value_with_opt_unit: value=float? unit=(space+ unit)? -> ValueWithUnit { 
        let unit = unit.unwrap_or_default();
        let value = value.unwrap_or_default();
        if value <= 0.0f32 {
            return Err(ParseFormError::InvalidGridUnit(value, unit));
        }

        // match unit {
        //     UnitOfMeasurement::DOTS | UnitOfMeasurement::XDOTS => {
        //         if value != value.floor() {
        //             return Err(ParseFormError::InvalidGridUnit(value, unit));
        //         }
        //     }
        //     _ => {}
        // }

        ValueWithUnit(value, unit)
    }

    grid_value_unit: value=(omitted_value_with_dots | opt_value_with_opt_unit) -> GridUnit { GridUnit::Value(value) }
    grid_value_format: value=ident -> GridUnit { GridUnit::FormatId(value) }
    grid_value = (grid_value_unit | grid_value_format) -> GridUnit;

    grid: space* "GRID" grid_value=((space+ "UNIT")? (space+ "IS")? space+ grid_value)? origin=(space+ "ORIGIN" (space+ opt_value_with_opt_unit)? (space+ opt_value_with_opt_unit)?)? semicolon -> Command { 
        let grid_value = grid_value.unwrap_or_else(|| GridUnit::FormatId("FMT1".into()));
        let origin = match origin {
            None => Default::default(),
            Some((Some(y), Some(x))) => (y, x),
            Some((Some(y), None)) => (y, Default::default()),
            Some((None, Some(x))) => (Default::default(), x),
            Some((None, None)) => (Default::default(), Default::default()),
        };
        Command::Grid(grid_value, origin)
    }

    end: space* "END" semicolon -> Command { Command::End }

    command = (grid | end) -> Command;

    pub test_form: commands=command+ -> Vec<Command> { commands }
}

The problem is that I cannot accept decimal places when the unit are DOTS. So I had to add this verification.

match unit {
    UnitOfMeasurement::DOTS | UnitOfMeasurement::XDOTS => {
        if value != value.floor() {
            return Err(ParseFormError::InvalidGridUnit(value, unit));
        }
    }
    _ => {}
}

But once I do that, I get the error message:

> GRID DOTS ORIGIN 0.5 DOTS;

1 | GRID DOTS ORIGIN 0.5 DOTS;
                         ^
[1:21] Expected space

I think this is caused by trying to recover from the optional part of the rule:

 opt_value_with_opt_unit: value=float? unit=(space+ unit)? -> ValueWithUnit { 
                                             ^

I know that its trying to recover from a error, because if I put a debug print before the return err

match unit {
    UnitOfMeasurement::DOTS | UnitOfMeasurement::XDOTS => {
        if value != value.floor() {
            println!("error")
            return Err(ParseFormError::InvalidGridUnit(value, unit));
        }
    }
    _ => {}
}

You can see that it print the debug text.

Any help would be greatly appreciated. Thanks.

WinnetDev avatar Jun 23 '25 17:06 WinnetDev

This is not related to error recovery; you have no patterns here which can be recovered to. Only full rules wrapped in literals, and delimited lists, can be recovered from.

You're right that this most likely is a bug with the error prioritization, which I'll have to look into. However, I think you should try to encode this logic into the patterns rather than returning an error after the value has already been parsed. I would recommend using a match which can parse a value followed by a unit which is valid for that value.

I probably won't be able to look into this until next week, so I'll leave the issue open for now. It would be very helpful to me if you can minimize the complexity of the parser required to produce this bug, and show me that.

boxbeam avatar Jun 26 '25 20:06 boxbeam

New test case


use std::{default, num::{ParseFloatError}, ops::Range};
use untwine::{parser, parser_repl, prelude::Recoverable};

#[derive(Debug, Default)]
pub enum UnitOfMeasurement {
    DOTS,
    #[default]
    XDOTS
}

#[derive(Debug, Default)]
pub struct ValueWithUnit(f32, UnitOfMeasurement);

#[derive(Debug, thiserror::Error)]
pub enum ParseFormError {
    #[error(transparent)]
    Untwine(#[from] untwine::ParserError),
    #[error("Failed to parse number: {0}")]
    ParseFloat(#[from] ParseFloatError),
    #[error("Invalid Grid Unit: {0} {1:?}")]
    InvalidGridUnit(f32, UnitOfMeasurement)
}

parser! {
    [error = ParseFormError, recover = true]
    space = #{char::is_ascii_whitespace};
    semicolon = space* ';';

    digit = '0'-'9' -> char;
    float: num=<"-"? digit+ ("." digit+)?> -> f32 { num.parse()? }

    unit_dots: "DOTS" -> UnitOfMeasurement { UnitOfMeasurement::DOTS }
    unit_xdots: "XDOTS" -> UnitOfMeasurement { UnitOfMeasurement::XDOTS }
    unit = (unit_dots | unit_xdots) -> UnitOfMeasurement;

    pub opt_value_with_opt_unit: value=float? unit=(space+ unit)? -> ValueWithUnit { 
        let unit = unit.unwrap_or_default();
        let value = value.unwrap_or_default();
        if value <= 0.0f32 {
            return Err(ParseFormError::InvalidGridUnit(value, unit));
        }

        match unit {
            UnitOfMeasurement::DOTS | UnitOfMeasurement::XDOTS => {
                if value != value.floor() {
                    return Err(ParseFormError::InvalidGridUnit(value, unit));
                }
            }
            _ => {}
        }

        ValueWithUnit(value, unit)
    }
}

fn main() {
    parser_repl(opt_value_with_opt_unit);
}

Prompt

1.2 DOTS

Error

1 | 1.2 DOTS
        ^
[1:4] Expected space

WinnetDev avatar Jun 27 '25 19:06 WinnetDev

Thank you very much for providing a minimal example to reproduce the error!

boxbeam avatar Jul 10 '25 22:07 boxbeam