Skip to content

Commit e88e78f

Browse files
authored
Require parenthesis around parameters to procedures (#77)
In Technique v1 we require `(` and `)` around the parameters if listed in a procedure declaration. This branch improves the parser to emit an error if there is something wrong at this point on the line.
2 parents d2a98b8 + 6c608be commit e88e78f

File tree

4 files changed

+118
-21
lines changed

4 files changed

+118
-21
lines changed

src/parsing/parser.rs

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub enum ParsingError {
3939
InvalidForma(usize),
4040
InvalidGenus(usize),
4141
InvalidSignature(usize),
42+
InvalidParameters(usize),
4243
InvalidDeclaration(usize),
4344
InvalidSection(usize),
4445
InvalidInvocation(usize),
@@ -76,6 +77,7 @@ impl ParsingError {
7677
ParsingError::InvalidGenus(offset) => *offset,
7778
ParsingError::InvalidSignature(offset) => *offset,
7879
ParsingError::InvalidDeclaration(offset) => *offset,
80+
ParsingError::InvalidParameters(offset) => *offset,
7981
ParsingError::InvalidSection(offset) => *offset,
8082
ParsingError::InvalidInvocation(offset) => *offset,
8183
ParsingError::InvalidFunction(offset) => *offset,
@@ -281,15 +283,12 @@ impl<'i> Parser<'i> {
281283
.push(error);
282284
}
283285
}
284-
} else if self
285-
.source
286-
.contains(':')
287-
{
286+
} else if potential_procedure_declaration(self.source) {
288287
// It might be that we've encountered a malformed procedure
289288
// declaration, so we try parsing it anyway to get a more
290289
// specific error message.
291290
match self.take_block_lines(
292-
|_| true, // Accept the line regardless
291+
potential_procedure_declaration,
293292
|line| is_section(line) || potential_procedure_declaration(line),
294293
|inner| inner.read_procedure(),
295294
) {
@@ -856,6 +855,25 @@ impl<'i> Parser<'i> {
856855

857856
(name, parameters)
858857
} else {
858+
// Check if there are multiple words (procedure name + anything
859+
// else) which would indicates parameters without parentheses
860+
let words: Vec<&str> = text
861+
.trim()
862+
.split_whitespace()
863+
.collect();
864+
if words.len() > 1 {
865+
// Calculate position of first mistaken parameter-ish thing
866+
let first_space_pos = text
867+
.find(' ')
868+
.unwrap_or(0);
869+
let first_param_pos = text[first_space_pos..]
870+
.trim_start()
871+
.as_ptr() as isize
872+
- text.as_ptr() as isize;
873+
let error_offset = self.offset + one.start() + first_param_pos as usize;
874+
return Err(ParsingError::InvalidParameters(error_offset));
875+
}
876+
859877
let name = validate_identifier(text).ok_or(ParsingError::InvalidIdentifier(
860878
self.offset,
861879
text.to_string(),
@@ -2559,24 +2577,37 @@ fn is_procedure_declaration(content: &str) -> bool {
25592577
/// reporting what turns out to be a better error.
25602578
fn potential_procedure_declaration(content: &str) -> bool {
25612579
match content.split_once(':') {
2562-
Some((before, _after)) => {
2580+
Some((before, after)) => {
25632581
let before = before.trim_ascii();
2564-
// Check if it looks like an identifier (possibly with parameters)
2565-
// Accept any single token that could be an attempted identifier
2566-
if let Some((name, params)) = before.split_once('(') {
2567-
// Has parameters: check if params end with ')'
2568-
!name
2582+
2583+
// Empty before colon -> only a declaration if there's something after
2584+
if before.is_empty() {
2585+
return !after
25692586
.trim_ascii()
2570-
.is_empty()
2571-
&& params.ends_with(')')
2572-
} else {
2573-
// No parameters: must be a single token (no spaces) that
2574-
// looks identifier-ish This excludes sentences like "Ask
2575-
// these questions: ..."
2576-
!before.is_empty() &&
2577-
!before.contains(' ') && // Single token only
2578-
before.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
2587+
.is_empty();
2588+
}
2589+
2590+
// Has parentheses -> likely trying to be a procedure with parameters
2591+
if before.contains('(') {
2592+
return true;
25792593
}
2594+
2595+
// Check if it looks like prose vs an identifier attempt
2596+
// Prose typically: starts with capital, has multiple space-separated words
2597+
// Identifiers: lowercase, possibly with underscores
2598+
let first_char = before
2599+
.chars()
2600+
.next()
2601+
.unwrap();
2602+
let has_spaces = before.contains(' ');
2603+
2604+
// If it starts with uppercase AND has spaces, it's probably prose
2605+
if first_char.is_uppercase() && has_spaces {
2606+
return false;
2607+
}
2608+
2609+
// Otherwise, could be a procedure declaration attempt
2610+
true
25802611
}
25812612
None => false,
25822613
}

src/problem/messages.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,65 @@ Finally, variables can be assigned for the names of the input parameters:
350350
.to_string(),
351351
)
352352
}
353+
ParsingError::InvalidParameters(_) => {
354+
let examples = vec![
355+
Procedure {
356+
name: Identifier("create_bypass"),
357+
parameters: Some(vec![Identifier("a"), Identifier("b")]),
358+
signature: None,
359+
elements: Vec::new(),
360+
},
361+
Procedure {
362+
name: Identifier("bulldoze"),
363+
parameters: Some(vec![Identifier("c")]),
364+
signature: None,
365+
elements: Vec::new(),
366+
},
367+
Procedure {
368+
name: Identifier("lawsuit"),
369+
parameters: None,
370+
signature: Some(Signature {
371+
domain: Genus::Single(Forma("Council")),
372+
range: Genus::List(Forma("Penny")),
373+
}),
374+
elements: Vec::new(),
375+
},
376+
Procedure {
377+
name: Identifier("lawsuit"),
378+
parameters: Some(vec![Identifier("c")]),
379+
signature: Some(Signature {
380+
domain: Genus::Single(Forma("Council")),
381+
range: Genus::List(Forma("Penny")),
382+
}),
383+
elements: Vec::new(),
384+
},
385+
];
386+
387+
(
388+
"Parameters must be enclosed in parentheses".to_string(),
389+
format!(
390+
r#"
391+
Parameters to a procedure must be variables, and enclosed in parentheses. For
392+
example:
393+
394+
{}
395+
{}
396+
397+
Naming the input genus is optional, however; these are both valid procedure
398+
declarations (and in fact the same):
399+
400+
{}
401+
{}
402+
"#,
403+
examples[0].present(renderer),
404+
examples[1].present(renderer),
405+
examples[2].present(renderer),
406+
examples[3].present(renderer)
407+
)
408+
.trim_ascii()
409+
.to_string(),
410+
)
411+
}
353412
ParsingError::InvalidSection(_) => {
354413
// Roman numeral sections don't have AST representation
355414
(
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
% technique v1
2+
3+
make_coffee beans : Beans -> Espresso
4+
5+
make_coffee beans water : Beans, Water -> Cappuccino
6+
7+
make_coffee Beans, Water :

tests/parsing/errors.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ make-coffee : Ingredients -> Coffee
6969
make coffee : Ingredients -> Coffee
7070
"#
7171
.trim_ascii(),
72-
ParsingError::InvalidIdentifier(0, "".to_string()),
72+
ParsingError::InvalidParameters(0),
7373
);
7474
}
7575

0 commit comments

Comments
 (0)