Skip to content

Commit

Permalink
Tuple types and tuple expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
mohammadfawaz committed Jul 11, 2023
1 parent 5d0f987 commit 969a9bc
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 21 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> ::= <ident> "." [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 {
name: Ident,
index: usize,
},
}

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

fn format_expected_found_error<'a>(
Expand Down Expand Up @@ -120,6 +122,7 @@ impl<'a> CompileError<'a> {
ParseError::ExpectedFound { span, .. } => span.clone(),
ParseError::KeywordAsIdent { span, .. } => span.clone(),
ParseError::UntypedDecisionVar { span, .. } => span.clone(),
ParseError::InvalidTupleIndex { span, .. } => span.clone(),
},
}
}
Expand Down
24 changes: 20 additions & 4 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 @@ -266,6 +269,19 @@ fn lex_one_error(src: &str) -> CompileError {
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]
fn reals() {
assert_eq!(lex_one_success("1.05"), Token::RealLiteral("1.05"));
Expand All @@ -275,12 +291,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
105 changes: 95 additions & 10 deletions yurtc/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,36 @@ 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);

// This extracts a `usize` index. Fails for negative, hex, and binary literals.
// Recovers with `0`.
let index = select! {
Token::IntLiteral(num_str) => (Token::IntLiteral(num_str), num_str.parse::<usize>()),
}
.validate(|(token, index_result), span, emit| {
index_result.unwrap_or_else(|_| {
emit(ParseError::InvalidTupleIndex { span, index: token });
0
})
});

let tuple_index_expr = ident()
.then_ignore(just(Token::Dot))
.then(index)
.map(|(name, index)| ast::Expr::TupleIndex { name, index });

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_index_expr,
tuple,
ident().map(ast::Expr::Ident),
));

Expand Down Expand Up @@ -321,12 +342,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 +402,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 +887,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 +929,43 @@ 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#"t.0 + t.9999999"#),
expect_test::expect![[
r#"BinaryOp { op: Add, lhs: TupleIndex { name: Ident("t"), index: 0 }, rhs: TupleIndex { name: Ident("t"), index: 9999999 } }"#
]],
);

check(
&run_parser!(expr(), r#"t.0xa + t.0b1"#),
expect_test::expect![[r#"
@2..5: Invalid index value "0xa"
@10..13: Invalid index value "0b1"
"#]],
);
}

#[test]
fn basic_program() {
let src = r#"
Expand All @@ -907,7 +992,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 +1009,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 969a9bc

Please sign in to comment.