From affce34f1a85d576923f849f68a042f1b3aaf390 Mon Sep 17 00:00:00 2001 From: Mohammad Fawaz Date: Mon, 10 Jul 2023 15:18:39 -0400 Subject: [PATCH] Tuple types, tuple expressions, and tuple indexing --- specs/src/lang/language_primitives.md | 23 ++-- yurtc/src/ast.rs | 6 + yurtc/src/error.rs | 6 + yurtc/src/lexer.rs | 28 +++-- yurtc/src/parser.rs | 162 ++++++++++++++++++++++++-- 5 files changed, 197 insertions(+), 28 deletions(-) diff --git a/specs/src/lang/language_primitives.md b/specs/src/lang/language_primitives.md index 5630ddc25..481715fbd 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..7da575a90 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 { + tuple: Box, + index: usize, + }, } #[derive(Clone, Debug, PartialEq)] diff --git a/yurtc/src/error.rs b/yurtc/src/error.rs index 0705a82d2..ef097a38f 100644 --- a/yurtc/src/error.rs +++ b/yurtc/src/error.rs @@ -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>( @@ -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(), }, } } diff --git a/yurtc/src/lexer.rs b/yurtc/src/lexer.rs index 972ae0108..93422b90b 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"), @@ -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] @@ -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)], [])"#]], ); } diff --git a/yurtc/src/parser.rs b/yurtc/src/parser.rs index f61b0f775..8c47a4f3e 100644 --- a/yurtc/src/parser.rs +++ b/yurtc/src/parser.rs @@ -213,22 +213,48 @@ 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); + 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, ast::Expr, Error = ParseError<'sc>> + Clone +where + P: Parser, 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::() + .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, ast::Expr, Error = ParseError<'sc>> + Clone @@ -321,12 +347,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 +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( @@ -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""# ]], ); } @@ -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#" @@ -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" "#]], ); } @@ -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" "#]], ); }