• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

forst-lang / forst / 16034193780

02 Jul 2025 07:37PM UTC coverage: 37.619% (+0.3%) from 37.308%
16034193780

Pull #17

github

haveyaseen
fix(typechecker): implement proper nested field path traversal for shape guards

The typechecker was failing to handle nested field access with dot notation (e.g., op.input.name) in shape guards and other contexts. The root cause was that field lookup functions were not properly handling multi-segment field names and were incorrectly recursing into nested shapes for single-segment lookups.

This commit adds new path-aware field lookup functions (lookupFieldPath, lookupFieldPathOnShape, lookupFieldPathOnMergedFields) that properly handle dot notation by splitting field names and recursively traversing the path. The existing field lookup functions have been updated to use these new functions where appropriate, ensuring consistent behavior across type definitions, assertions, and variable lookups.

The fix specifically addresses the bug where looking up a field like "input" in a shape containing {input: {name: String}} would incorrectly try to find "input" in the nested shape instead of returning the shape type. For multi-segment paths like "input.name", the new logic properly splits the path and recurses into the nested shape to find "name".

All typechecker tests now pass, confirming that nested field access works correctly for shape guards and other use cases. The shape guard feature can now properly handle expressions like op.input.name where op is a shape with nested fields.
Pull Request #17: feat: Shape guards

1757 of 4800 new or added lines in 83 files covered. (36.6%)

89 existing lines in 29 files now uncovered.

3081 of 8190 relevant lines covered (37.62%)

6.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

44.26
/forst/internal/parser/function.go
1
package parser
2

3
import (
4
        "forst/internal/ast"
5
)
6

UNCOV
7
func (p *Parser) parseParameterType() ast.TypeNode {
×
UNCOV
8
        next := p.peek()
×
UNCOV
9
        // TODO: Even if the next token is a dot this could be a type without any constraints
×
UNCOV
10
        if next.Type == ast.TokenDot || next.Type == ast.TokenLParen {
×
11
                assertion := p.parseAssertionChain(true)
×
12
                return ast.TypeNode{
×
13
                        Ident:     ast.TypeAssertion,
×
14
                        Assertion: &assertion,
×
15
                }
×
16
        }
×
17
        // Disallow Shape({...}) wrapper for shape types
NEW
18
        if p.current().Type == ast.TokenIdentifier && p.current().Value == "Shape" {
×
NEW
19
                ident := p.expect(ast.TokenIdentifier)
×
NEW
20
                if p.current().Type == ast.TokenLParen {
×
NEW
21
                        p.FailWithParseError(p.current(), "Shape({...}) wrapper is not allowed. Use {...} directly for shape types.")
×
NEW
22
                }
×
NEW
23
                return ast.TypeNode{
×
NEW
24
                        Ident: ast.TypeIdent(ident.Value),
×
NEW
25
                }
×
26
        }
27
        // Allow direct {...} for shape types
NEW
28
        if p.current().Type == ast.TokenLBrace {
×
NEW
29
                shape := p.parseShape(nil)
×
NEW
30
                baseType := ast.TypeIdent(ast.TypeShape)
×
NEW
31
                return ast.TypeNode{
×
NEW
32
                        Ident: ast.TypeShape,
×
NEW
33
                        Assertion: &ast.AssertionNode{
×
NEW
34
                                BaseType: &baseType,
×
NEW
35
                                Constraints: []ast.ConstraintNode{{
×
NEW
36
                                        Name: "Match",
×
NEW
37
                                        Args: []ast.ConstraintArgumentNode{{
×
NEW
38
                                                Shape: &shape,
×
NEW
39
                                        }},
×
NEW
40
                                }},
×
NEW
41
                        },
×
NEW
42
                }
×
NEW
43
        }
×
UNCOV
44
        return p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
×
45
}
46

47
func (p *Parser) parseDestructuredParameter() ast.ParamNode {
×
48
        p.expect(ast.TokenLBrace)
×
49
        fields := []string{}
×
50

×
51
        // Parse fields until we hit closing brace
×
52
        for p.current().Type != ast.TokenRBrace {
×
53
                name := p.expect(ast.TokenIdentifier)
×
54
                fields = append(fields, name.Value)
×
55

×
56
                // Handle comma between fields
×
57
                if p.current().Type == ast.TokenComma {
×
58
                        p.advance()
×
59
                }
×
60
        }
61

62
        p.expect(ast.TokenRBrace)
×
63
        p.expect(ast.TokenColon)
×
64

×
65
        paramType := p.parseParameterType()
×
66

×
67
        return ast.DestructuredParamNode{
×
68
                Fields: fields,
×
69
                Type:   paramType,
×
70
        }
×
71
}
72

73
func (p *Parser) parseSimpleParameter() ast.ParamNode {
4✔
74
        name := p.expect(ast.TokenIdentifier).Value
4✔
75
        // Remove colon requirement - use Go-style parameter declarations
4✔
76

4✔
77
        tok := p.current()
4✔
78
        if tok.Type == ast.TokenIdentifier && (p.peek().Type == ast.TokenDot || p.peek().Type == ast.TokenLParen) {
4✔
NEW
79
                assertion := p.parseAssertionChain(true)
×
NEW
80
                return ast.SimpleParamNode{
×
NEW
81
                        Ident: ast.Ident{ID: ast.Identifier(name)},
×
NEW
82
                        Type: ast.TypeNode{
×
NEW
83
                                Ident:     ast.TypeAssertion,
×
NEW
84
                                Assertion: &assertion,
×
NEW
85
                        },
×
NEW
86
                }
×
NEW
87
        }
×
88

89
        if tok.Type == ast.TokenIdentifier && tok.Value == "Shape" {
4✔
NEW
90
                // Check if this is Shape({...})
×
NEW
91
                if p.peek().Type == ast.TokenLParen {
×
NEW
92
                        p.FailWithParseError(tok, "Direct usage of Shape({...}) is not allowed. Use a shape type directly, e.g. { field: Type }.")
×
NEW
93
                }
×
94
                // Allow direct usage of Shape as a type name
UNCOV
95
                p.advance()
×
NEW
96
                typeIdent := ast.TypeIdent("Shape")
×
NEW
97
                return ast.SimpleParamNode{
×
NEW
98
                        Ident: ast.Ident{ID: ast.Identifier(name)},
×
NEW
99
                        Type:  ast.TypeNode{Ident: typeIdent},
×
NEW
100
                }
×
101
        }
102
        if tok.Type == ast.TokenLBrace {
4✔
NEW
103
                shape := p.parseShape(nil)
×
NEW
104
                baseType := ast.TypeIdent(ast.TypeShape)
×
NEW
105
                return ast.SimpleParamNode{
×
NEW
106
                        Ident: ast.Ident{ID: ast.Identifier(name)},
×
NEW
107
                        Type: ast.TypeNode{
×
NEW
108
                                Ident: ast.TypeShape,
×
NEW
109
                                Assertion: &ast.AssertionNode{
×
NEW
110
                                        BaseType: &baseType,
×
NEW
111
                                        Constraints: []ast.ConstraintNode{{
×
NEW
112
                                                Name: "Match",
×
NEW
113
                                                Args: []ast.ConstraintArgumentNode{{
×
NEW
114
                                                        Shape: &shape,
×
NEW
115
                                                }},
×
NEW
116
                                        }},
×
NEW
117
                                },
×
NEW
118
                        },
×
NEW
119
                }
×
UNCOV
120
        }
×
121
        // Parse the type, which may include dots (e.g. AppMutation.Input)
122
        typ := p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
4✔
123
        p.logParsedNodeWithMessage(typ, "Parsed parameter type, next token: "+p.current().Type.String()+" ("+p.current().Value+")")
4✔
124
        return ast.SimpleParamNode{
4✔
125
                Ident: ast.Ident{ID: ast.Identifier(name)},
4✔
126
                Type:  typ,
4✔
127
        }
4✔
128
}
129

130
func (p *Parser) parseParameter() ast.ParamNode {
4✔
131
        switch p.current().Type {
4✔
132
        case ast.TokenIdentifier:
4✔
133
                return p.parseSimpleParameter()
4✔
134
        case ast.TokenLBrace:
×
135
                return p.parseDestructuredParameter()
×
136
        default:
×
NEW
137
                p.FailWithParseError(p.current(), "Expected parameter")
×
NEW
138
                panic("Reached unreachable path")
×
139
        }
140
}
141

142
// Parse function parameters
143
func (p *Parser) parseFunctionSignature() []ast.ParamNode {
30✔
144
        p.expect(ast.TokenLParen)
30✔
145
        params := []ast.ParamNode{}
30✔
146

30✔
147
        // Handle empty parameter list
30✔
148
        if p.current().Type == ast.TokenRParen {
57✔
149
                p.advance()
27✔
150
                return params
27✔
151
        }
27✔
152

153
        // Parse parameters
154
        for {
6✔
155
                param := p.parseParameter()
3✔
156
                switch param.(type) {
3✔
157
                case ast.DestructuredParamNode:
×
NEW
158
                        p.logParsedNodeWithMessage(param, "Parsed destructured function param")
×
159
                default:
3✔
160
                        p.logParsedNodeWithMessage(param, "Parsed function param")
3✔
161
                }
162
                params = append(params, param)
3✔
163

3✔
164
                // Check if there are more parameters
3✔
165
                if p.current().Type == ast.TokenComma {
3✔
166
                        p.advance()
×
167
                } else {
3✔
168
                        break
3✔
169
                }
170
        }
171

172
        p.expect(ast.TokenRParen)
3✔
173
        return params
3✔
174
}
175

176
func (p *Parser) parseReturnType() []ast.TypeNode {
30✔
177
        returnType := []ast.TypeNode{}
30✔
178
        if p.current().Type == ast.TokenColon {
37✔
179
                p.advance() // Consume the colon
7✔
180
                returnType = append(returnType, p.parseType(TypeIdentOpts{AllowLowercaseTypes: false}))
7✔
181
        }
7✔
182
        return returnType
30✔
183
}
184

185
func (p *Parser) parseReturnStatement() ast.ReturnNode {
21✔
186
        p.advance() // Move past `return`
21✔
187

21✔
188
        returnExpression := p.parseExpression()
21✔
189

21✔
190
        return ast.ReturnNode{
21✔
191
                Value: returnExpression,
21✔
192
                Type:  ast.TypeNode{Ident: ast.TypeImplicit},
21✔
193
        }
21✔
194
}
21✔
195

196
func (p *Parser) parseFunctionBody() []ast.Node {
30✔
197
        return p.parseBlock()
30✔
198
}
30✔
199

200
// Parse a function definition
201
func (p *Parser) parseFunctionDefinition() ast.FunctionNode {
30✔
202
        p.expect(ast.TokenFunc)               // Expect `fn`
30✔
203
        name := p.expect(ast.TokenIdentifier) // Function name
30✔
204

30✔
205
        p.context.ScopeStack.CurrentScope().FunctionName = name.Value
30✔
206

30✔
207
        params := p.parseFunctionSignature() // Parse function parameters
30✔
208

30✔
209
        returnType := p.parseReturnType()
30✔
210

30✔
211
        body := p.parseFunctionBody()
30✔
212

30✔
213
        node := ast.FunctionNode{
30✔
214
                Ident:       ast.Ident{ID: ast.Identifier(name.Value)},
30✔
215
                ReturnTypes: returnType,
30✔
216
                Params:      params,
30✔
217
                Body:        body,
30✔
218
        }
30✔
219

30✔
220
        return node
30✔
221
}
30✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc