Repository: gleam-lang/gleam (Rust)

Issue:
Incorrect parsing of nested tuple access
<!--
Please include the following:
- Steps and code to reproduce the problem
- Versions for Gleam, OS, the runtime and any other software used
- Logs from running Gleam with the `GLEAM_LOG` environment variable set to `trace`
-->
As discussed in discord, there is likely a parser bug that is causing nested tuple access to result in odd compilation errors, as well as the formatter putting the access (`0.0` in the below example) on a new line.


With the following[ gleam code](https://playground.gleam.run/#N4IgbgpgTgzglgewHYgFwEYA0IDGyAuES+aIAZkgAT4CuADjAPpzEAUAlJQLQB8lAxK0EBJYu0wDWo/O07AAOlUmD04wQCZZigL6LFdGgCNKFSgFsAhiw69K0ygqW0GzNuwB0ABi869KbUA=): 
```gleam
fn tups_int() -> #(#(Int), #(Int)) {
  #(#(1),#(2))
}
pub fn main() -> Int {
  tups_int().0.0
}
```
The compiler is reporting this error:

```
error: Unknown record field
   ┌─ /src/main.gleam:13:13
   │
13 │   tups_int().0.0
   │             ^ This field does not exist

The value being accessed has this type:

    #(#(Int), #(Int))

It does not have any fields.

error: Type mismatch
   ┌─ /src/main.gleam:13:14
   │
13 │   tups_int().0.0
   │              ^^^

The type of this returned value doesn't match the return type
annotation of this function.

Expected type:

    Int

Found type:

    Float
 ```


Discussion:
Thank you. This is a regression, let's fix it.
Just checked in v1.1 (when nested tuple access was first introduced). This code never compiled successfully

The problem is because [`maybe_lex_dot_access`](https://github.com/gleam-lang/gleam/blob/639b4f6c313d7a4e347d525cc10547e00123d86f/compiler-core/src/parse/lexer.rs#L647), the function responsible for nested tuple parsing is [only called after a name token](https://github.com/gleam-lang/gleam/blob/639b4f6c313d7a4e347d525cc10547e00123d86f/compiler-core/src/parse/lexer.rs#L157). I propose we call it after a dot token instead.

I'll look into fixing this when I get time



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9677a04701..691a688144f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -214,6 +214,10 @@
   not emit a warning if an @external function had been referenced.
   ([Richard Viney](https://github.com/richard-viney))
 
+- Fixed a bug where nested tuple access would not be parsed correctly when
+  the left-hand side was a function call.
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ## v1.6.1 - 2024-11-19
 
 ### Bug fixed
diff --git a/compiler-core/src/parse/lexer.rs b/compiler-core/src/parse/lexer.rs
index bc14cf61e1e..004a71bc48e 100644
--- a/compiler-core/src/parse/lexer.rs
+++ b/compiler-core/src/parse/lexer.rs
@@ -154,7 +154,6 @@ where
                 check_for_minus = true;
                 let name = self.lex_name()?;
                 self.emit(name);
-                self.maybe_lex_dot_access()?;
             } else if self.is_number_start(c, self.chr1) {
                 check_for_minus = true;
                 let num = self.lex_number()?;
@@ -437,6 +436,7 @@ where
                 } else {
                     let tok_end = self.get_pos();
                     self.emit((tok_start, Token::Dot, tok_end));
+                    self.maybe_lex_dot_access()?;
                 }
             }
             '#' => {
@@ -647,8 +647,7 @@ where
     fn maybe_lex_dot_access(&mut self) -> Result<(), LexicalError> {
         // It can be nested like: `tuple.1.2.3.4`
         loop {
-            if Some('.') == self.chr0 && matches!(self.chr1, Some('0'..='9')) {
-                self.eat_single_char(Token::Dot);
+            if matches!(self.chr0, Some('0'..='9')) {
                 let number = self.lex_int_number()?;
                 self.emit(number);
             } else {


Test patch:
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuple_access_after_function.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuple_access_after_function.snap
new file mode 100644
index 00000000000..2382accddf4
--- /dev/null
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuple_access_after_function.snap
@@ -0,0 +1,36 @@
+---
+source: compiler-core/src/parse/tests.rs
+expression: tuple().0.1
+---
+[
+    Expression(
+        TupleIndex {
+            location: SrcSpan {
+                start: 0,
+                end: 11,
+            },
+            index: 1,
+            tuple: TupleIndex {
+                location: SrcSpan {
+                    start: 0,
+                    end: 9,
+                },
+                index: 0,
+                tuple: Call {
+                    location: SrcSpan {
+                        start: 0,
+                        end: 7,
+                    },
+                    fun: Var {
+                        location: SrcSpan {
+                            start: 0,
+                            end: 5,
+                        },
+                        name: "tuple",
+                    },
+                    arguments: [],
+                },
+            },
+        },
+    ),
+]
diff --git a/compiler-core/src/parse/tests.rs b/compiler-core/src/parse/tests.rs
index b1731ee63ac..8a6bafc54f3 100644
--- a/compiler-core/src/parse/tests.rs
+++ b/compiler-core/src/parse/tests.rs
@@ -1672,3 +1672,9 @@ type Wibble {
 "#
     );
 }
+
+#[test]
+// https://github.com/gleam-lang/gleam/issues/3870
+fn nested_tuple_access_after_function() {
+    assert_parse!("tuple().0.1");
+}