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

forst-lang / forst / 16102887825

06 Jul 2025 08:28PM UTC coverage: 40.653% (+1.3%) from 39.364%
16102887825

push

github

web-flow
fix: Enhanced Field Merging and Constraint Processing (#26)

284 of 537 new or added lines in 14 files covered. (52.89%)

2 existing lines in 1 file now uncovered.

3984 of 9800 relevant lines covered (40.65%)

6.8 hits per line

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

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

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

7
func (p *Parser) parseParameterType() ast.TypeNode {
×
8
        next := p.peek()
×
9
        // TODO: Even if the next token is a dot this could be a type without any constraints
×
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
18
        if p.current().Type == ast.TokenIdentifier && p.current().Value == "Shape" {
×
19
                ident := p.expect(ast.TokenIdentifier)
×
20
                if p.current().Type == ast.TokenLParen {
×
21
                        p.FailWithParseError(p.current(), "Shape({...}) wrapper is not allowed. Use {...} directly for shape types.")
×
22
                }
×
23
                return ast.TypeNode{
×
24
                        Ident: ast.TypeIdent(ident.Value),
×
25
                }
×
26
        }
27
        // Allow direct {...} for shape types
28
        if p.current().Type == ast.TokenLBrace {
×
NEW
29
                shape := p.parseShapeType()
×
30
                baseType := ast.TypeIdent(ast.TypeShape)
×
31
                return ast.TypeNode{
×
32
                        Ident: ast.TypeShape,
×
33
                        Assertion: &ast.AssertionNode{
×
34
                                BaseType: &baseType,
×
35
                                Constraints: []ast.ConstraintNode{{
×
36
                                        Name: "Match",
×
37
                                        Args: []ast.ConstraintArgumentNode{{
×
38
                                                Shape: &shape,
×
39
                                        }},
×
40
                                }},
×
41
                        },
×
42
                }
×
43
        }
×
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 {
5✔
74
        name := p.expect(ast.TokenIdentifier).Value
5✔
75
        // Remove colon requirement - use Go-style parameter declarations
5✔
76

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

89
        if tok.Type == ast.TokenIdentifier && tok.Value == "Shape" {
5✔
90
                // Check if this is Shape({...})
×
91
                if p.peek().Type == ast.TokenLParen {
×
92
                        p.FailWithParseError(tok, "Direct usage of Shape({...}) is not allowed. Use a shape type directly, e.g. { field: Type }.")
×
93
                }
×
94
                // Allow direct usage of Shape as a type name
95
                p.advance()
×
96
                typeIdent := ast.TypeIdent("Shape")
×
97
                return ast.SimpleParamNode{
×
98
                        Ident: ast.Ident{ID: ast.Identifier(name)},
×
99
                        Type:  ast.TypeNode{Ident: typeIdent},
×
100
                }
×
101
        }
102
        if tok.Type == ast.TokenLBrace {
6✔
103
                shape := p.parseShapeType()
1✔
104
                baseType := ast.TypeIdent(ast.TypeShape)
1✔
105
                return ast.SimpleParamNode{
1✔
106
                        Ident: ast.Ident{ID: ast.Identifier(name)},
1✔
107
                        Type: ast.TypeNode{
1✔
108
                                Ident: ast.TypeShape,
1✔
109
                                Assertion: &ast.AssertionNode{
1✔
110
                                        BaseType: &baseType,
1✔
111
                                        Constraints: []ast.ConstraintNode{{
1✔
112
                                                Name: "Match",
1✔
113
                                                Args: []ast.ConstraintArgumentNode{{
1✔
114
                                                        Shape: &shape,
1✔
115
                                                }},
1✔
116
                                        }},
1✔
117
                                },
1✔
118
                        },
1✔
119
                }
1✔
120
        }
1✔
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 {
5✔
131
        switch p.current().Type {
5✔
132
        case ast.TokenIdentifier:
5✔
133
                return p.parseSimpleParameter()
5✔
134
        case ast.TokenLBrace:
×
135
                return p.parseDestructuredParameter()
×
136
        default:
×
137
                p.FailWithParseError(p.current(), "Expected parameter")
×
138
                panic("Reached unreachable path")
×
139
        }
140
}
141

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

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

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

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

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

176
func (p *Parser) parseReturnType() []ast.TypeNode {
32✔
177
        returnType := []ast.TypeNode{}
32✔
178
        if p.current().Type == ast.TokenColon {
39✔
179
                p.advance() // Consume the colon
7✔
180
                // Support both single and parenthesized multiple return types
7✔
181
                if p.current().Type == ast.TokenLParen {
7✔
182
                        p.advance() // Consume '('
×
183
                        for {
×
184
                                typ := p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
×
185
                                returnType = append(returnType, typ)
×
186
                                if p.current().Type == ast.TokenComma {
×
187
                                        p.advance()
×
188
                                } else {
×
189
                                        break
×
190
                                }
191
                        }
192
                        p.expect(ast.TokenRParen)
×
193
                } else {
7✔
194
                        returnType = append(returnType, p.parseType(TypeIdentOpts{AllowLowercaseTypes: false}))
7✔
195
                }
7✔
196
        }
197
        return returnType
32✔
198
}
199

200
func (p *Parser) parseReturnStatement() ast.ReturnNode {
22✔
201
        p.advance() // Move past `return`
22✔
202

22✔
203
        // Parse multiple return values
22✔
204
        values := []ast.ExpressionNode{}
22✔
205

22✔
206
        // Parse first expression
22✔
207
        if p.current().Type != ast.TokenSemicolon && p.current().Type != ast.TokenRBrace {
44✔
208
                values = append(values, p.parseExpression())
22✔
209
        }
22✔
210

211
        // Parse additional expressions separated by commas
212
        for p.current().Type == ast.TokenComma {
23✔
213
                p.advance() // Consume comma
1✔
214
                values = append(values, p.parseExpression())
1✔
215
        }
1✔
216

217
        return ast.ReturnNode{
22✔
218
                Values: values,
22✔
219
                Type:   ast.TypeNode{Ident: ast.TypeImplicit},
22✔
220
        }
22✔
221
}
222

223
func (p *Parser) parseFunctionBody() []ast.Node {
32✔
224
        return p.parseBlock()
32✔
225
}
32✔
226

227
// Parse a function definition
228
func (p *Parser) parseFunctionDefinition() ast.FunctionNode {
32✔
229
        p.expect(ast.TokenFunc)               // Expect `fn`
32✔
230
        name := p.expect(ast.TokenIdentifier) // Function name
32✔
231

32✔
232
        p.context.ScopeStack.CurrentScope().FunctionName = name.Value
32✔
233

32✔
234
        params := p.parseFunctionSignature() // Parse function parameters
32✔
235

32✔
236
        returnType := p.parseReturnType()
32✔
237

32✔
238
        body := p.parseFunctionBody()
32✔
239

32✔
240
        node := ast.FunctionNode{
32✔
241
                Ident:       ast.Ident{ID: ast.Identifier(name.Value)},
32✔
242
                ReturnTypes: returnType,
32✔
243
                Params:      params,
32✔
244
                Body:        body,
32✔
245
        }
32✔
246

32✔
247
        return node
32✔
248
}
32✔
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