From 969a9bc1c4744334efe8d192816310501bf01494 Mon Sep 17 00:00:00 2001 From: Mohammad Fawaz Date: Mon, 10 Jul 2023 15:18:39 -0400 Subject: [PATCH] Tuple types and tuple expressions --- specs/src/lang/language_primitives.md | 23 ++++-- yurtc/src/ast.rs | 6 ++ yurtc/src/error.rs | 3 + yurtc/src/lexer.rs | 24 +++++- yurtc/src/parser.rs | 105 +++++++++++++++++++++++--- 5 files changed, 140 insertions(+), 21 deletions(-) diff --git a/specs/src/lang/language_primitives.md b/specs/src/lang/language_primitives.md index 5630ddc25..255e2504a 100644 --- a/specs/src/lang/language_primitives.md +++ b/specs/src/lang/language_primitives.md @@ -62,7 +62,7 @@ Identifiers have the following syntax: ::= _?[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 @@ -138,7 +138,8 @@ Expressions represent values and have the following syntax: | | | - | + | + | | | ``` @@ -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 - ::= "(" "," [ "," ... ] ")" + ::= "(" "," [ "," ... ] ")" ``` -For example: `let t = (5, 3, "foo")`; +For example: `let t = (5, 3, "foo");`. + +Tuple indexing expressions are written as: + +```ebnf + ::= "." [0-9]+ +``` + +For example: `let second = t.1;` which extracts the second element of tuple `t` and stores it into `second`. #### "If" Expressions @@ -279,7 +288,7 @@ Call expressions are used to call functions and have the following syntax: ::= "(" ( "," ... ) ")" ``` -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. diff --git a/yurtc/src/ast.rs b/yurtc/src/ast.rs index 42ca6e977..f7f3c5aec 100644 --- a/yurtc/src/ast.rs +++ b/yurtc/src/ast.rs @@ -41,6 +41,7 @@ pub(super) enum Type { Int, Bool, String, + Tuple(Vec), } #[derive(Clone, Debug, PartialEq)] @@ -62,6 +63,11 @@ pub(super) enum Expr { }, Block(Block), If(IfExpr), + Tuple(Vec), + TupleIndex { + name: Ident, + index: usize, + }, } #[derive(Clone, Debug, PartialEq)] diff --git a/yurtc/src/error.rs b/yurtc/src/error.rs index 0705a82d2..ae5169808 100644 --- a/yurtc/src/error.rs +++ b/yurtc/src/error.rs @@ -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>( @@ -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(), }, } } diff --git a/yurtc/src/lexer.rs b/yurtc/src/lexer.rs index 972ae0108..0e13b73d2 100644 --- a/yurtc/src/lexer.rs +++ b/yurtc/src/lexer.rs @@ -49,6 +49,8 @@ pub(super) enum Token<'sc> { ParenClose, #[token("->")] Arrow, + #[token(".")] + Dot, #[token("real")] Real, @@ -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"), @@ -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")); @@ -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)], [])"#]], ); } diff --git a/yurtc/src/parser.rs b/yurtc/src/parser.rs index f61b0f775..7e35519f7 100644 --- a/yurtc/src/parser.rs +++ b/yurtc/src/parser.rs @@ -213,15 +213,36 @@ fn expr<'sc>() -> impl Parser, 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::()), + } + .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), )); @@ -321,12 +342,20 @@ fn ident<'sc>() -> impl Parser, ast::Ident, Error = ParseError<'sc>> } fn type_<'sc>() -> impl Parser, 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, ast::Immediate, Error = ParseError<'sc>> + Clone { @@ -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( @@ -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""# ]], ); } @@ -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#" @@ -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" "#]], ); } @@ -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" "#]], ); }