Skip to content

Commit

Permalink
Parse tuple types and expressions (#93)
Browse files Browse the repository at this point in the history
Closes #72 

Fairly simple change:
- Tuple types
- Tuple expressions
- Tuple indexing expressions. Error out on invalid indices such as `0xa`
or anything that is not a `usize`.

Note that `t.0.0` does not currently work, but `t.0 .0`. Once we
implement #66, we can then write `(t.0).0`. In the future, we can
support `t.0.0` which can be done in two ways:
- Special case the lexer to _not_ parse the `0.0` as a real in the case
of a tuple access (this means the lexer now has to concern itself with
the context).
- Break the real `0.0` apart in the pasrer, which kinda what Rust does
(see rust-lang/rust#71322 after an attempt for
the other method in rust-lang/rust#70420)
  • Loading branch information
mohammadfawaz authored Jul 12, 2023
2 parents 5d0f987 + affce34 commit 98db342
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 28 deletions.
23 changes: 16 additions & 7 deletions specs/src/lang/language_primitives.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Identifiers have the following syntax:
<ident> ::= _?[A-Za-z][A-Za-z0-9]* % excluding keywords
```

A number of keywords are reserved and cannot be used as identifiers. The keywords are: `bool`, `constraint`, `else`, `false`, `real`, `fn`, `if`, `int`, `let`, `maximize`, `minimize`, `satisfy`, `solve`, `true`, `type`.
A number of keywords are reserved and cannot be used as identifiers. The keywords are: `bool`, `constraint`, `else`, `false`, `real`, `fn`, `if`, `int`, `let`, `maximize`, `minimize`, `satisfy`, `solve`, `true`.

## High-level Intent Structure

Expand Down Expand Up @@ -138,7 +138,8 @@ Expressions represent values and have the following syntax:
| <int-literal>
| <real-literal>
| <string-literal>
| <tuple-literal>
| <tuple-expr>
| <tuple-index-expr>
| <if-expr>
| <call-expr>
```
Expand Down Expand Up @@ -249,15 +250,23 @@ let string = "first line\
third line";
```

#### Tuple Literals
#### Tuple Expressions and Tuple Indexing Expressions

Tuple literals are written as:
Tuple Expressions are written as:

```ebnf
<tuple-literal> ::= "(" <expr> "," [ <expr> "," ... ] ")"
<tuple-expr> ::= "(" <expr> "," [ <expr> "," ... ] ")"
```

For example: `let t = (5, 3, "foo")`;
For example: `let t = (5, 3, "foo");`.

Tuple indexing expressions are written as:

```ebnf
<tuple-index-expr> ::= <expr-atom> "." [0-9]+
```

For example: `let second = t.1;` which extracts the second element of tuple `t` and stores it into `second`.

#### "If" Expressions

Expand All @@ -279,7 +288,7 @@ Call expressions are used to call functions and have the following syntax:
<call-expr> ::= <ident> "(" ( <expr> "," ... ) ")"
```

For example, `x = foo(5, 2);`
For example: `x = foo(5, 2);`.

The type of the expressions passed as arguments must match the argument types of the called function. The return type of the function must also be appropriate for the calling context.

Expand Down
6 changes: 6 additions & 0 deletions yurtc/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub(super) enum Type {
Int,
Bool,
String,
Tuple(Vec<Type>),
}

#[derive(Clone, Debug, PartialEq)]
Expand All @@ -62,6 +63,11 @@ pub(super) enum Expr {
},
Block(Block),
If(IfExpr),
Tuple(Vec<Expr>),
TupleIndex {
tuple: Box<Expr>,
index: usize,
},
}

#[derive(Clone, Debug, PartialEq)]
Expand Down
6 changes: 6 additions & 0 deletions yurtc/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ pub(super) enum ParseError<'a> {
"type annotation or initializer needed for decision variable \"{}\"", name.0
)]
UntypedDecisionVar { span: Span, name: ast::Ident },
#[error("Invalid integer value \"{}\" for tuple index", index)]
InvalidIntegerForTupleIndex { span: Span, index: Token<'a> },
#[error("Invalid value \"{}\" for tuple index", index)]
InvalidTupleIndex { span: Span, index: Token<'a> },
}

fn format_expected_found_error<'a>(
Expand Down Expand Up @@ -120,6 +124,8 @@ impl<'a> CompileError<'a> {
ParseError::ExpectedFound { span, .. } => span.clone(),
ParseError::KeywordAsIdent { span, .. } => span.clone(),
ParseError::UntypedDecisionVar { span, .. } => span.clone(),
ParseError::InvalidIntegerForTupleIndex { span, .. } => span.clone(),
ParseError::InvalidTupleIndex { span, .. } => span.clone(),
},
}
}
Expand Down
28 changes: 18 additions & 10 deletions yurtc/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub(super) enum Token<'sc> {
ParenClose,
#[token("->")]
Arrow,
#[token(".")]
Dot,

#[token("real")]
Real,
Expand Down Expand Up @@ -150,6 +152,7 @@ impl<'sc> fmt::Display for Token<'sc> {
Token::ParenOpen => write!(f, "("),
Token::ParenClose => write!(f, ")"),
Token::Arrow => write!(f, "->"),
Token::Dot => write!(f, "."),
Token::Real => write!(f, "real"),
Token::Int => write!(f, "int"),
Token::Bool => write!(f, "bool"),
Expand Down Expand Up @@ -258,12 +261,17 @@ fn lex_one_success(src: &str) -> Token<'_> {
toks[0].0.clone()
}

#[cfg(test)]
fn lex_one_error(src: &str) -> CompileError {
// Tokenise src, assume a single error.
let (_, errs) = lex(src);
assert_eq!(errs.len(), 1, "Testing for single error only.");
errs[0].clone()
#[test]
fn control_tokens() {
assert_eq!(lex_one_success(":"), Token::Colon);
assert_eq!(lex_one_success(";"), Token::Semi);
assert_eq!(lex_one_success(","), Token::Comma);
assert_eq!(lex_one_success("{"), Token::BraceOpen);
assert_eq!(lex_one_success("}"), Token::BraceClose);
assert_eq!(lex_one_success("("), Token::ParenOpen);
assert_eq!(lex_one_success(")"), Token::ParenClose);
assert_eq!(lex_one_success("->"), Token::Arrow);
assert_eq!(lex_one_success("."), Token::Dot);
}

#[test]
Expand All @@ -275,12 +283,12 @@ fn reals() {
assert_eq!(lex_one_success("0.34"), Token::RealLiteral("0.34"));
assert_eq!(lex_one_success("-0.34"), Token::RealLiteral("-0.34"));
check(
&format!("{:?}", lex_one_error(".34")),
expect_test::expect![[r#"Lex { span: 0..1, error: InvalidToken }"#]],
&format!("{:?}", lex(".34")),
expect_test::expect![[r#"([(Dot, 0..1), (IntLiteral("34"), 1..3)], [])"#]],
);
check(
&format!("{:?}", lex_one_error("12.")),
expect_test::expect!["Lex { span: 2..3, error: InvalidToken }"],
&format!("{:?}", lex("12.")),
expect_test::expect![[r#"([(IntLiteral("12"), 0..2), (Dot, 2..3)], [])"#]],
);
}

Expand Down
162 changes: 151 additions & 11 deletions yurtc/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,22 +213,48 @@ fn expr<'sc>() -> impl Parser<Token<'sc>, ast::Expr, Error = ParseError<'sc>> +
.delimited_by(just(Token::ParenOpen), just(Token::ParenClose));

let call = ident()
.then(args)
.then(args.clone())
.map(|(name, args)| ast::Expr::Call { name, args });

let tuple = args.map(ast::Expr::Tuple);

let atom = choice((
immediate().map(ast::Expr::Immediate),
unary_op(expr.clone()),
code_block_expr(expr.clone()).map(ast::Expr::Block),
if_expr(expr.clone()),
call,
tuple,
ident().map(ast::Expr::Ident),
));

comparison_op(additive_op(multiplicative_op(atom)))
comparison_op(additive_op(multiplicative_op(tuple_index(atom))))
})
}

fn tuple_index<'sc, P>(
parser: P,
) -> impl Parser<Token<'sc>, ast::Expr, Error = ParseError<'sc>> + Clone
where
P: Parser<Token<'sc>, ast::Expr, Error = ParseError<'sc>> + Clone,
{
// This extracts a `usize` index. Fails for everything else (therefore, `t.0.0` is not
// supported - but `t.0 .0` is fine).
let index = filter_map(|span, token| match token {
Token::IntLiteral(num_str) => num_str
.parse::<usize>()
.map_err(|_| ParseError::InvalidIntegerForTupleIndex { span, index: token }),
_ => Err(ParseError::InvalidTupleIndex { span, index: token }),
});

parser
.then(just(Token::Dot).ignore_then(index).repeated())
.foldl(|expr, index| ast::Expr::TupleIndex {
tuple: Box::new(expr),
index,
})
}

fn multiplicative_op<'sc, P>(
parser: P,
) -> impl Parser<Token<'sc>, ast::Expr, Error = ParseError<'sc>> + Clone
Expand Down Expand Up @@ -321,12 +347,20 @@ fn ident<'sc>() -> impl Parser<Token<'sc>, ast::Ident, Error = ParseError<'sc>>
}

fn type_<'sc>() -> impl Parser<Token<'sc>, ast::Type, Error = ParseError<'sc>> + Clone {
choice((
just(Token::Real).to(ast::Type::Real),
just(Token::Int).to(ast::Type::Int),
just(Token::Bool).to(ast::Type::Bool),
just(Token::String).to(ast::Type::String),
))
recursive(|type_| {
let tuple = type_
.separated_by(just(Token::Comma))
.allow_trailing()
.delimited_by(just(Token::ParenOpen), just(Token::ParenClose));

choice((
just(Token::Real).to(ast::Type::Real),
just(Token::Int).to(ast::Type::Int),
just(Token::Bool).to(ast::Type::Bool),
just(Token::String).to(ast::Type::String),
tuple.map(ast::Type::Tuple),
))
})
}

fn immediate<'sc>() -> impl Parser<Token<'sc>, ast::Immediate, Error = ParseError<'sc>> + Clone {
Expand Down Expand Up @@ -373,6 +407,25 @@ fn check(actual: &str, expect: expect_test::Expect) {
expect.assert_eq(actual);
}

#[test]
fn types() {
check(&run_parser!(type_(), "int"), expect_test::expect!["Int"]);
check(&run_parser!(type_(), "real"), expect_test::expect!["Real"]);
check(&run_parser!(type_(), "bool"), expect_test::expect!["Bool"]);
check(
&run_parser!(type_(), "string"),
expect_test::expect!["String"],
);
check(
&run_parser!(type_(), "(int, real, string)"),
expect_test::expect!["Tuple([Int, Real, String])"],
);
check(
&run_parser!(type_(), "(int, (real, int), string)"),
expect_test::expect!["Tuple([Int, Tuple([Real, Int]), String])"],
);
}

#[test]
fn let_decls() {
check(
Expand Down Expand Up @@ -839,7 +892,7 @@ fn code_blocks() {
check(
&format!("{:?}", run_parser!(let_decl(expr()), "let x = {};")),
expect_test::expect![[
r#""@9..10: found \"}\" but expected \"!\", \"+\", \"-\", \"{\", \"if\", \"var\", \"let\", or \"constraint\"\n""#
r#""@9..10: found \"}\" but expected \"!\", \"+\", \"-\", \"{\", \"(\", \"if\", \"var\", \"let\", or \"constraint\"\n""#
]],
);
}
Expand Down Expand Up @@ -881,6 +934,93 @@ fn if_exprs() {
);
}

#[test]
fn tuple_expressions() {
check(
&run_parser!(expr(), r#"(0,)"#),
expect_test::expect!["Tuple([Immediate(Int(0))])"],
);

check(
&run_parser!(expr(), r#"(0, 1.0, "foo")"#),
expect_test::expect![[
r#"Tuple([Immediate(Int(0)), Immediate(Real(1.0)), Immediate(String("foo"))])"#
]],
);

check(
&run_parser!(expr(), r#"(0, (1.0, "bar"), "foo")"#),
expect_test::expect![[
r#"Tuple([Immediate(Int(0)), Tuple([Immediate(Real(1.0)), Immediate(String("bar"))]), Immediate(String("foo"))])"#
]],
);

check(
&run_parser!(expr(), r#"( { 42 }, if cond { 2 } else { 3 }, foo() )"#),
expect_test::expect![[
r#"Tuple([Block(Block { statements: [], final_expr: Immediate(Int(42)) }), If(IfExpr { condition: Ident(Ident("cond")), then_block: Block { statements: [], final_expr: Immediate(Int(2)) }, else_block: Block { statements: [], final_expr: Immediate(Int(3)) } }), Call { name: Ident("foo"), args: [] }])"#
]],
);

check(
&run_parser!(expr(), r#"t.0 + t.9999999"#),
expect_test::expect![[
r#"BinaryOp { op: Add, lhs: TupleIndex { tuple: Ident(Ident("t")), index: 0 }, rhs: TupleIndex { tuple: Ident(Ident("t")), index: 9999999 } }"#
]],
);

check(
&run_parser!(expr(), r#"(0, 1).0"#),
expect_test::expect![
"TupleIndex { tuple: Tuple([Immediate(Int(0)), Immediate(Int(1))]), index: 0 }"
],
);

check(
&run_parser!(expr(), r#"t.0 .0"#),
expect_test::expect![[
r#"TupleIndex { tuple: TupleIndex { tuple: Ident(Ident("t")), index: 0 }, index: 0 }"#
]],
);

check(
&run_parser!(expr(), r#"foo().0"#),
expect_test::expect![[
r#"TupleIndex { tuple: Call { name: Ident("foo"), args: [] }, index: 0 }"#
]],
);

check(
&run_parser!(expr(), r#"{ (0, 0) }.0"#),
expect_test::expect!["TupleIndex { tuple: Block(Block { statements: [], final_expr: Tuple([Immediate(Int(0)), Immediate(Int(0))]) }), index: 0 }"],
);

check(
&run_parser!(expr(), r#"if true { (0, 0) } else { (0, 0) }.0"#),
expect_test::expect!["TupleIndex { tuple: If(IfExpr { condition: Immediate(Bool(true)), then_block: Block { statements: [], final_expr: Tuple([Immediate(Int(0)), Immediate(Int(0))]) }, else_block: Block { statements: [], final_expr: Tuple([Immediate(Int(0)), Immediate(Int(0))]) } }), index: 0 }"],
);

// This parses because `1 + 2` is an expression, but it should fail in semantic analysis.
check(
&run_parser!(expr(), "1 + 2 .3"),
expect_test::expect!["BinaryOp { op: Add, lhs: Immediate(Int(1)), rhs: TupleIndex { tuple: Immediate(Int(2)), index: 3 } }"],
);

check(
&run_parser!(let_decl(expr()), "let x = t.0xa;"),
expect_test::expect![[r#"
@10..13: Invalid integer value "0xa" for tuple index
"#]],
);

check(
&run_parser!(let_decl(expr()), "let x = t.xx;"),
expect_test::expect![[r#"
@10..12: Invalid value "xx" for tuple index
"#]],
);
}

#[test]
fn basic_program() {
let src = r#"
Expand All @@ -907,7 +1047,7 @@ fn with_errors() {
check(
&run_parser!(yurt_program(), "let low_val: bad = 1.23"),
expect_test::expect![[r#"
@13..16: found "bad" but expected "real", "int", "bool", or "string"
@13..16: found "bad" but expected "(", "real", "int", "bool", or "string"
"#]],
);
}
Expand All @@ -924,7 +1064,7 @@ fn fn_errors() {
check(
&run_parser!(yurt_program(), "fn foo() -> real {}"),
expect_test::expect![[r#"
@18..19: found "}" but expected "!", "+", "-", "{", "if", "var", "let", or "constraint"
@18..19: found "}" but expected "!", "+", "-", "{", "(", "if", "var", "let", or "constraint"
"#]],
);
}
Expand Down

0 comments on commit 98db342

Please sign in to comment.