From c34e57f3e007516c7f79b543ae9c14663f942b04 Mon Sep 17 00:00:00 2001 From: monsterkrampe Date: Thu, 5 Jun 2025 10:46:27 +0200 Subject: [PATCH] Add naive AF support in Web --- frontend/src/components/adf-new-form.tsx | 21 +++- frontend/src/help-texts/add-info.md | 14 +++ server/src/adf.rs | 127 +++++++++++++++++++++-- 3 files changed, 150 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/adf-new-form.tsx b/frontend/src/components/adf-new-form.tsx index af2841c..cd518ba 100644 --- a/frontend/src/components/adf-new-form.tsx +++ b/frontend/src/components/adf-new-form.tsx @@ -40,6 +40,7 @@ function AdfNewForm({ fetchProblems }: { fetchProblems: () => void; }) { const [code, setCode] = useState(PLACEHOLDER); const [filename, setFilename] = useState(''); const [parsing, setParsing] = useState('Naive'); + const [isAf, setIsAf] = useState(false); const [name, setName] = useState(''); const fileRef = useRef(null); @@ -59,6 +60,7 @@ function AdfNewForm({ fetchProblems }: { fetchProblems: () => void; }) { } formData.append('parsing', parsing); + formData.append('is_af', isAf); formData.append('name', name); fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/add`, { @@ -119,10 +121,13 @@ function AdfNewForm({ fetchProblems }: { fetchProblems: () => void; }) { label="Put your code here:" helperText={( <> - For more info on the syntax, have a + For more info on the ADF syntax, have a look {' '} here + . For the AF syntax, we currently only allow the ICCMA competition format, see for example + {' '} + here . )} @@ -137,6 +142,20 @@ function AdfNewForm({ fetchProblems }: { fetchProblems: () => void; }) { + + ADF or AF? + setIsAf(((e.target as HTMLInputElement).value))} + > + } label="ADF" /> + } label="AF" /> + + AFs are converted to ADFs internally. + Parsing Strategy >, // Either Code or File is set file: Option, // Either Code or File is set parsing: Text, + is_af: Text, // if its not an AF then it is an ADF } #[derive(Clone)] @@ -222,6 +226,7 @@ struct AddAdfProblemBodyPlain { name: String, code: String, parsing: Parsing, + is_af: bool, // if its not an AF then it is an ADF } impl TryFrom for AddAdfProblemBodyPlain { @@ -237,6 +242,7 @@ impl TryFrom for AddAdfProblemBodyPlain { .and_then(|code| (!code.is_empty()).then_some(code)) .ok_or("Either a file or the code has to be provided.")?, parsing: source.parsing.into_inner(), + is_af: source.is_af.into_inner(), }) } } @@ -259,6 +265,7 @@ struct AdfProblemInfo { name: String, code: String, parsing_used: Parsing, + is_af: bool, acs_per_strategy: AcsPerStrategy, running_tasks: Vec, } @@ -269,6 +276,7 @@ impl AdfProblemInfo { name: adf.name.clone(), code: adf.code, parsing_used: adf.parsing_used, + is_af: adf.is_af, acs_per_strategy: adf.acs_per_strategy, running_tasks: tasks .iter() @@ -280,6 +288,91 @@ impl AdfProblemInfo { } } +struct AF(Vec>); + +impl From for AdfParser { + fn from(source: AF) -> Self { + let names: Vec = (0..source.0.len()) + .map(|val| (val + 1).to_string()) + .collect(); + let dict: HashMap = names + .iter() + .enumerate() + .map(|(i, val)| (val.clone(), i)) + .collect(); + let formulae: Vec = source + .0 + .into_iter() + .map(|attackers| { + attackers.into_iter().fold( + // TODO: is it correct to use Top here? what if something is not attacked at all? + Formula::Top, + |acc, attacker| { + Formula::And( + Box::new(acc), + Box::new(Formula::Not(Box::new(Formula::Atom( + (attacker + 1).to_string(), + )))), + ) + }, + ) + }) + .collect(); + let formulanames = names.clone(); + + Self { + namelist: Arc::new(RwLock::new(names)), + dict: Arc::new(RwLock::new(dict)), + formulae: RefCell::new(formulae), + formulaname: RefCell::new(formulanames), + } + } +} + +fn parse_af(code: String) -> Result { + let mut lines = code.lines(); + + let Some(first_line) = lines.next() else { + return Err("There must be at least one line in the AF input."); + }; + + let first_line: Vec<_> = first_line.split(" ").collect(); + if first_line[0] != "p" || first_line[1] != "af" { + return Err("Expected first line to be of the form: p af "); + } + + let Ok(num_arguments) = first_line[2].parse::() else { + return Err("Could not convert number of arguments to u32; expected first line to be of the form: p af "); + }; + + let attacks_opt: Option> = lines + .filter(|line| !line.starts_with('#') && !line.is_empty()) + .map(|line| { + let mut line = line.split(" "); + let a = line.next()?; + let b = line.next()?; + if line.next().is_some() { + None + } else { + Some((a.parse::().ok()?, b.parse::().ok()?)) + } + }) + .collect(); + let Some(attacks) = attacks_opt else { + return Err("Line must be of the form: n m"); + }; + + // index in outer vector represents attacked element + let mut is_attacked_by: Vec> = vec![vec![]; num_arguments]; + for (a, b) in attacks { + is_attacked_by[b - 1].push(a - 1); // we normalize names to be zero-indexed + } + + let hacked_adf_parser = AdfParser::from(AF(is_attacked_by)); + + Ok(hacked_adf_parser) +} + #[post("/add")] async fn add_adf_problem( req: HttpRequest, @@ -381,6 +474,7 @@ async fn add_adf_problem( username: username.clone(), code: adf_problem_input.code.clone(), parsing_used: adf_problem_input.parsing, + is_af: adf_problem_input.is_af, adf: SimplifiedAdfOpt::None, acs_per_strategy: AcsPerStrategy::default(), }; @@ -413,9 +507,20 @@ async fn add_adf_problem( #[cfg(feature = "mock_long_computations")] std::thread::sleep(Duration::from_secs(20)); - let parser = AdfParser::default(); - let parse_result = parser.parse()(&adf_problem_input.code) - .map_err(|_| "ADF could not be parsed, double check your input!"); + let (parser, parse_result) = { + if adf_problem_input.is_af { + parse_af(adf_problem_input.code) + .map(|p| (p, Ok(()))) + .unwrap_or_else(|e| (AdfParser::default(), Err(e))) + } else { + let parser = AdfParser::default(); + let parse_result = parser.parse()(&adf_problem_input.code) + .map(|_| ()) + .map_err(|_| "ADF could not be parsed, double check your input!"); + + (parser, parse_result) + } + }; let result = parse_result.map(|_| { let lib_adf = match adf_problem_input.parsing { @@ -500,7 +605,7 @@ async fn solve_adf_problem( .collection(ADF_COLL); let username = match identity.map(|id| id.id()) { - None => { + Option::None => { return HttpResponse::Unauthorized().body("You need to login to add an ADF problem.") } Some(Err(err)) => return HttpResponse::InternalServerError().body(err.to_string()), @@ -512,7 +617,7 @@ async fn solve_adf_problem( .await { Err(err) => return HttpResponse::InternalServerError().body(err.to_string()), - Ok(None) => { + Ok(Option::None) => { return HttpResponse::NotFound() .body(format!("ADF problem with name {problem_name} not found.")) } @@ -644,7 +749,7 @@ async fn get_adf_problem( .collection(ADF_COLL); let username = match identity.map(|id| id.id()) { - None => { + Option::None => { return HttpResponse::Unauthorized().body("You need to login to get an ADF problem.") } Some(Err(err)) => return HttpResponse::InternalServerError().body(err.to_string()), @@ -656,7 +761,7 @@ async fn get_adf_problem( .await { Err(err) => return HttpResponse::InternalServerError().body(err.to_string()), - Ok(None) => { + Ok(Option::None) => { return HttpResponse::NotFound() .body(format!("ADF problem with name {problem_name} not found.")) } @@ -682,7 +787,7 @@ async fn delete_adf_problem( .collection(ADF_COLL); let username = match identity.map(|id| id.id()) { - None => { + Option::None => { return HttpResponse::Unauthorized().body("You need to login to get an ADF problem.") } Some(Err(err)) => return HttpResponse::InternalServerError().body(err.to_string()), @@ -717,7 +822,7 @@ async fn get_adf_problems_for_user( .collection(ADF_COLL); let username = match identity.map(|id| id.id()) { - None => { + Option::None => { return HttpResponse::Unauthorized().body("You need to login to get an ADF problem.") } Some(Err(err)) => return HttpResponse::InternalServerError().body(err.to_string()),