-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathparser.go
More file actions
3560 lines (3063 loc) · 102 KB
/
parser.go
File metadata and controls
3560 lines (3063 loc) · 102 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package parser
// Copyright (c) 2025-Present Marshall A Burns
// Licensed under the MIT License. See LICENSE for details.
import (
"fmt"
"math/big"
"strconv"
"strings"
. "github.com/marshallburns/ez/pkg/ast"
"github.com/marshallburns/ez/pkg/errors"
. "github.com/marshallburns/ez/pkg/lexer"
. "github.com/marshallburns/ez/pkg/tokenizer"
)
// Reserved keywords that cannot be used as identifiers
var reservedKeywords = map[string]bool{
"temp": true, "const": true, "do": true, "return": true,
"if": true, "or": true, "otherwise": true,
"for": true, "for_each": true, "as_long_as": true, "loop": true,
"break": true, "continue": true, "in": true, "not_in": true, "range": true,
"import": true, "using": true, "struct": true, "enum": true,
"nil": true, "new": true, "true": true, "false": true,
"module": true, "private": true, "from": true, "use": true,
"ensure": true,
}
// Builtin function and type names that cannot be redefined
var builtinNames = map[string]bool{
// Builtin functions
"len": true, "typeof": true, "input": true,
"println": true, "print": true, "read_int": true,
"copy": true, "append": true, "error": true,
// Primitive type names
"int": true, "float": true, "string": true, "bool": true, "char": true, "byte": true,
// Sized integers
"i8": true, "i16": true, "i32": true, "i64": true, "i128": true, "i256": true,
"u8": true, "u16": true, "u32": true, "u64": true, "u128": true, "u256": true, "uint": true,
// Sized floats
"f32": true, "f64": true,
// Collection types
"map": true,
}
// isReservedName checks if a name is a reserved keyword or builtin
func isReservedName(name string) bool {
return reservedKeywords[name] || builtinNames[name]
}
// allWarningCodes contains all valid warning codes in the language
var allWarningCodes = map[string]bool{
// Special
"ALL": true, // suppress all warnings
// Code Style Warnings (W1xxx)
"W1001": true, // unused-variable
"W1002": true, // unused-import
"W1003": true, // unused-function
"W1004": true, // unused-parameter
// Potential Bug Warnings (W2xxx)
"W2001": true, // unreachable-code
"W2002": true, // shadowed-variable
"W2003": true, // missing-return
"W2004": true, // implicit-type-conversion
"W2005": true, // deprecated-feature
"W2006": true, // byte-overflow-potential
"W2009": true, // nil-dereference-potential
// Code Quality Warnings (W3xxx)
"W3001": true, // empty-block
"W3002": true, // redundant-condition
"W3003": true, // array-size-mismatch
// Module Warnings (W4xxx)
"W4001": true, // module-name-mismatch
}
// suppressibleWarnings contains warning codes that can be suppressed via #suppress
// These are function-body warnings that make sense to suppress at the function level
var suppressibleWarnings = map[string]bool{
"ALL": true, // suppress all warnings
"W1001": true, // unused-variable
"W1004": true, // unused-parameter
"W2001": true, // unreachable-code
"W2002": true, // shadowed-variable
"W2003": true, // missing-return
"W2004": true, // implicit-type-conversion
"W2005": true, // deprecated-feature
"W2006": true, // byte-overflow-potential
"W2009": true, // nil-dereference-potential
"W3001": true, // empty-block
"W3002": true, // redundant-condition
"W3003": true, // array-size-mismatch
}
// isValidWarningCode checks if a warning code exists
func isValidWarningCode(code string) bool {
return allWarningCodes[code]
}
// isWarningSuppressible checks if a warning code can be suppressed
func isWarningSuppressible(code string) bool {
return suppressibleWarnings[code]
}
// hasSuppressAttribute checks if attributes contain a #suppress attribute
func hasSuppressAttribute(attrs []*Attribute) *Attribute {
for _, attr := range attrs {
if attr.Name == "suppress" {
return attr
}
}
return nil
}
// hasStrictAttribute checks if attributes contain a #strict attribute
func hasStrictAttribute(attrs []*Attribute) *Attribute {
for _, attr := range attrs {
if attr.Name == "strict" {
return attr
}
}
return nil
}
// parseFileLevelSuppress parses a file-level #suppress(...) and returns the warning codes
func (p *Parser) parseFileLevelSuppress() []string {
var codes []string
suppressToken := p.currentToken
p.nextToken() // move past #suppress
// Expect opening paren
if !p.currentTokenMatches(LPAREN) {
p.addEZError(errors.E2002, "#suppress requires parentheses with warning code(s)", suppressToken)
return codes
}
p.nextToken() // move past (
// Parse warning codes
for !p.currentTokenMatches(RPAREN) && !p.currentTokenMatches(EOF) {
if p.currentTokenMatches(STRING) || p.currentTokenMatches(IDENT) {
code := p.currentToken.Literal
// Validate the warning code
if !isValidWarningCode(code) {
p.addEZError(errors.E2052, fmt.Sprintf("%s is not a valid warning code", code), p.currentToken)
} else if !isWarningSuppressible(code) {
p.addEZError(errors.E2052, fmt.Sprintf("warning code %s cannot be suppressed", code), p.currentToken)
} else {
codes = append(codes, code)
}
p.nextToken()
} else if p.currentTokenMatches(COMMA) {
p.nextToken()
} else {
p.addEZError(errors.E2002, "expected warning code in #suppress", p.currentToken)
p.nextToken()
}
}
// Move past closing paren if present
if p.currentTokenMatches(RPAREN) {
// Don't advance - ParseProgram will call nextToken
}
return codes
}
// Operator precedence levels
const (
_ int = iota
LOWEST
OR_PREC // ||
AND_PREC // &&
EQUALS // ==, !=
LESSGREATER // <, >, <=, >=
MEMBERSHIP // in, not_in, !in
SUM // +, -
PRODUCT // *, /, %
PREFIX // -X, !X
POSTFIX // x++, x--
CALL // function(x)
INDEX // array[index]
MEMBER // object.member
)
// Setting the order in which tokens should be evaluated
var priorities = map[TokenType]int{
OR: OR_PREC,
AND: AND_PREC,
EQ: EQUALS,
NOT_EQ: EQUALS,
LT: LESSGREATER,
GT: LESSGREATER,
LT_EQ: LESSGREATER,
GT_EQ: LESSGREATER,
IN: MEMBERSHIP,
NOT_IN: MEMBERSHIP,
PLUS: SUM,
MINUS: SUM,
ASTERISK: PRODUCT,
SLASH: PRODUCT,
PERCENT: PRODUCT,
INCREMENT: POSTFIX,
DECREMENT: POSTFIX,
LPAREN: CALL,
LBRACKET: INDEX,
DOT: MEMBER,
}
// Function types for parsing expressions based on token position.
// prefixParseFn: parses tokens at the start of an expression (literals, identifiers, unary operators)
// infixParseFn: parses tokens between expressions, receives the left-hand side (binary operators, calls, indexing)
type (
prefixParseFn func() Expression
infixParseFn func(Expression) Expression
)
type Parser struct {
l *Lexer
errors []string
currentToken Token
peekToken Token
prefixParseFns map[TokenType]prefixParseFn
infixParseFns map[TokenType]infixParseFn
// New error system
ezErrors *errors.EZErrorList
source string
filename string
// Scope tracking for duplicate detection
scopes []map[string]Token // stack of scopes, each mapping name to declaration token
// Function scope tracking
functionDepth int // depth of nested function scopes (0 = global, >0 = inside function)
// Active suppressions from function/block attributes
activeSuppressions []*Attribute
}
// Scope management methods
func (p *Parser) pushScope() {
p.scopes = append(p.scopes, make(map[string]Token))
}
func (p *Parser) popScope() {
if len(p.scopes) > 1 {
p.scopes = p.scopes[:len(p.scopes)-1]
}
}
func (p *Parser) currentScope() map[string]Token {
return p.scopes[len(p.scopes)-1]
}
// declareInScope declares a name in current scope, returns error if duplicate
func (p *Parser) declareInScope(name string, token Token) bool {
scope := p.currentScope()
if _, exists := scope[name]; exists {
return false // duplicate
}
scope[name] = token
return true
}
// Returns a pointer to Parser(p)
// with initialized members
func New(l *Lexer) *Parser {
p := &Parser{
l: l,
errors: []string{},
ezErrors: errors.NewErrorList(),
source: "",
filename: "<unknown>",
scopes: []map[string]Token{make(map[string]Token)}, // start with global scope
}
p.prefixParseFns = make(map[TokenType]prefixParseFn)
p.setPrefix(IDENT, p.parseIdentifier)
p.setPrefix(INT, p.parseIntegerValue)
p.setPrefix(FLOAT, p.parseFloatValue)
p.setPrefix(STRING, p.parseStringValue)
p.setPrefix(RAW_STRING, p.parseRawStringValue)
p.setPrefix(CHAR, p.parseCharValue)
p.setPrefix(TRUE, p.parseBooleanValue)
p.setPrefix(FALSE, p.parseBooleanValue)
p.setPrefix(NIL, p.parseNilValue)
p.setPrefix(BANG, p.parsePrefixExpression)
p.setPrefix(MINUS, p.parsePrefixExpression)
p.setPrefix(LPAREN, p.parseGroupedExpression)
p.setPrefix(LBRACE, p.parseArrayValue)
p.setPrefix(NEW, p.parseNewExpression)
p.setPrefix(RANGE, p.parseRangeExpression)
p.setPrefix(CAST, p.parseCastExpression)
p.infixParseFns = make(map[TokenType]infixParseFn)
p.setInfix(PLUS, p.parseInfixExpression)
p.setInfix(MINUS, p.parseInfixExpression)
p.setInfix(ASTERISK, p.parseInfixExpression)
p.setInfix(SLASH, p.parseInfixExpression)
p.setInfix(PERCENT, p.parseInfixExpression)
p.setInfix(EQ, p.parseInfixExpression)
p.setInfix(NOT_EQ, p.parseInfixExpression)
p.setInfix(LT, p.parseInfixExpression)
p.setInfix(GT, p.parseInfixExpression)
p.setInfix(LT_EQ, p.parseInfixExpression)
p.setInfix(GT_EQ, p.parseInfixExpression)
p.setInfix(AND, p.parseInfixExpression)
p.setInfix(OR, p.parseInfixExpression)
p.setInfix(IN, p.parseInfixExpression)
p.setInfix(NOT_IN, p.parseInfixExpression)
p.setInfix(LPAREN, p.parseCallExpression)
p.setInfix(LBRACKET, p.parseIndexExpression)
p.setInfix(DOT, p.parseMemberExpression)
p.setInfix(INCREMENT, p.parsePostfixExpression)
p.setInfix(DECREMENT, p.parsePostfixExpression)
// Read two tokens to initialize currentToken and peekToken
p.nextToken()
p.nextToken()
return p
}
// NewWithSource creates a parser with source context for better errors
func NewWithSource(l *Lexer, source, filename string) *Parser {
p := &Parser{
l: l,
errors: []string{},
ezErrors: errors.NewErrorList(),
source: source,
filename: filename,
scopes: []map[string]Token{make(map[string]Token)}, // start with global scope
}
p.prefixParseFns = make(map[TokenType]prefixParseFn)
p.setPrefix(IDENT, p.parseIdentifier)
p.setPrefix(INT, p.parseIntegerValue)
p.setPrefix(FLOAT, p.parseFloatValue)
p.setPrefix(STRING, p.parseStringValue)
p.setPrefix(RAW_STRING, p.parseRawStringValue)
p.setPrefix(CHAR, p.parseCharValue)
p.setPrefix(TRUE, p.parseBooleanValue)
p.setPrefix(FALSE, p.parseBooleanValue)
p.setPrefix(NIL, p.parseNilValue)
p.setPrefix(BANG, p.parsePrefixExpression)
p.setPrefix(MINUS, p.parsePrefixExpression)
p.setPrefix(LPAREN, p.parseGroupedExpression)
p.setPrefix(LBRACE, p.parseArrayValue)
p.setPrefix(NEW, p.parseNewExpression)
p.setPrefix(RANGE, p.parseRangeExpression)
p.setPrefix(CAST, p.parseCastExpression)
p.infixParseFns = make(map[TokenType]infixParseFn)
p.setInfix(PLUS, p.parseInfixExpression)
p.setInfix(MINUS, p.parseInfixExpression)
p.setInfix(ASTERISK, p.parseInfixExpression)
p.setInfix(SLASH, p.parseInfixExpression)
p.setInfix(PERCENT, p.parseInfixExpression)
p.setInfix(EQ, p.parseInfixExpression)
p.setInfix(NOT_EQ, p.parseInfixExpression)
p.setInfix(LT, p.parseInfixExpression)
p.setInfix(GT, p.parseInfixExpression)
p.setInfix(LT_EQ, p.parseInfixExpression)
p.setInfix(GT_EQ, p.parseInfixExpression)
p.setInfix(AND, p.parseInfixExpression)
p.setInfix(OR, p.parseInfixExpression)
p.setInfix(IN, p.parseInfixExpression)
p.setInfix(NOT_IN, p.parseInfixExpression)
p.setInfix(LPAREN, p.parseCallExpression)
p.setInfix(LBRACKET, p.parseIndexExpression)
p.setInfix(DOT, p.parseMemberExpression)
p.setInfix(INCREMENT, p.parsePostfixExpression)
p.setInfix(DECREMENT, p.parsePostfixExpression)
// Read two tokens to initialize currentToken and peekToken
p.nextToken()
p.nextToken()
return p
}
func (p *Parser) setPrefix(tType TokenType, fn prefixParseFn) {
p.prefixParseFns[tType] = fn
}
func (p *Parser) setInfix(tType TokenType, fn infixParseFn) {
p.infixParseFns[tType] = fn
}
func (p *Parser) Errors() []string {
return p.errors
}
// EZErrors returns the new error list
func (p *Parser) EZErrors() *errors.EZErrorList {
return p.ezErrors
}
// addEZError adds a formatted error to the error list
func (p *Parser) addEZError(code errors.ErrorCode, message string, tok Token) {
sourceLine := ""
if p.source != "" {
sourceLine = errors.GetSourceLine(p.source, tok.Line)
}
err := errors.NewErrorWithSource(
code,
message,
p.filename,
tok.Line,
tok.Column,
sourceLine,
)
err.EndColumn = tok.Column + len(tok.Literal)
p.ezErrors.AddError(err)
}
func (p *Parser) nextToken() {
p.currentToken = p.peekToken
p.peekToken = p.l.NextToken()
// Set file info on tokens for multi-file module support
if p.filename != "" && p.filename != "<unknown>" {
p.currentToken.File = p.filename
p.peekToken.File = p.filename
}
}
func (p *Parser) currentTokenMatches(tType TokenType) bool {
return p.currentToken.Type == tType
}
func (p *Parser) peekTokenMatches(tType TokenType) bool {
return p.peekToken.Type == tType
}
func (p *Parser) expectPeek(tType TokenType) bool {
if p.peekTokenMatches(tType) {
p.nextToken()
return true
}
p.peekError(tType)
return false
}
func (p *Parser) peekError(tType TokenType) {
var msg string
var code errors.ErrorCode
// Provide better error messages for unclosed delimiters
if p.peekToken.Type == EOF {
switch tType {
case RPAREN:
msg = "unclosed parenthesis - expected ')'"
code = errors.E2005
case RBRACE:
msg = "unclosed brace - expected '}'"
code = errors.E2004
case RBRACKET:
msg = "unclosed bracket - expected ']'"
code = errors.E2006
default:
msg = fmt.Sprintf("unexpected end of file, expected %s", tType)
code = errors.E2002
}
} else {
msg = fmt.Sprintf("expected %s, got %s instead", tType, p.peekToken.Type)
code = errors.E2002
}
p.errors = append(p.errors, msg)
p.addEZError(code, msg, p.peekToken)
}
func (p *Parser) peekPrecedence() int {
if p, ok := priorities[p.peekToken.Type]; ok {
return p
}
return LOWEST
}
func (p *Parser) curPrecedence() int {
if p, ok := priorities[p.currentToken.Type]; ok {
return p
}
return LOWEST
}
// ParseProgram parses the entire program
func (p *Parser) ParseProgram() *Program {
program := &Program{}
program.FileUsing = []*UsingStatement{}
program.Statements = []Statement{}
seenOtherDeclaration := false
seenModuleDecl := false
importedModules := make(map[string]bool) // Track imported modules
// Check for module declaration first (must be first non-comment token)
if p.currentTokenMatches(MODULE) {
program.Module = p.parseModuleDeclaration()
seenModuleDecl = true
p.nextToken()
}
for !p.currentTokenMatches(EOF) {
// Module declaration must come first if present
if p.currentTokenMatches(MODULE) {
if seenModuleDecl {
p.addEZError(errors.E2002, "duplicate module declaration", p.currentToken)
} else if seenOtherDeclaration {
p.addEZError(errors.E2002, "module declaration must be the first statement in the file", p.currentToken)
}
p.nextToken()
continue
}
// Handle file-level #suppress (must come after imports/using, before other declarations)
// If we haven't seen other declarations yet, treat #suppress as file-level
// If we have seen declarations, treat #suppress as function-level (let parseStatement handle it)
if p.currentTokenMatches(SUPPRESS) && !seenOtherDeclaration {
// Parse the #suppress(...) and store in program
suppressCodes := p.parseFileLevelSuppress()
program.FileSuppressWarnings = append(program.FileSuppressWarnings, suppressCodes...)
p.nextToken()
continue
}
// Track what we've seen for placement validation
if p.currentTokenMatches(USING) {
// File-scoped using: must come before other declarations
if seenOtherDeclaration {
p.addEZError(errors.E2009, "file-scoped 'using' must come before all declarations", p.currentToken)
p.nextToken()
continue
}
// Parse and add to FileUsing
usingStmt := p.parseUsingStatement()
if usingStmt != nil {
// Validate that all modules in the using statement have been imported
for _, module := range usingStmt.Modules {
if !importedModules[module.Value] {
msg := fmt.Sprintf("cannot use module '%s' before importing it", module.Value)
p.addEZError(errors.E2010, msg, usingStmt.Token)
}
}
program.FileUsing = append(program.FileUsing, usingStmt)
}
p.nextToken()
continue
} else if !p.currentTokenMatches(EOF) && !p.currentTokenMatches(IMPORT) {
seenOtherDeclaration = true
}
// Check for imports after other declarations (functions, types, etc.)
if p.currentTokenMatches(IMPORT) && seenOtherDeclaration {
p.addEZError(errors.E2036, "import statements must appear at the top of the file, before any declarations", p.currentToken)
}
stmt := p.parseStatement()
if stmt != nil {
program.Statements = append(program.Statements, stmt)
// Track imported modules by both alias and module name
if importStmt, ok := stmt.(*ImportStatement); ok {
// Handle multiple imports (new comma-separated syntax)
if len(importStmt.Imports) > 0 {
for _, item := range importStmt.Imports {
// Track the alias so "using arr" works with "import arr@arrays"
importedModules[item.Alias] = true
// Also track the module name so "using arrays" still works
importedModules[item.Module] = true
}
} else {
// Backward compatibility: track single import
importedModules[importStmt.Alias] = true
importedModules[importStmt.Module] = true
}
}
}
p.nextToken()
}
return program
}
// ParseLine parses a single line for REPL mode
// Returns a statement node or nil if the line is empty/comment-only
func (p *Parser) ParseLine() Statement {
// Skip if we're at EOF
if p.currentTokenMatches(EOF) {
return nil
}
stmt := p.parseStatement()
return stmt
}
func (p *Parser) parseStatement() Statement {
// Check for #suppress, #strict, #enum(...), #flags attributes
var attrs []*Attribute
if p.currentTokenMatches(SUPPRESS) || p.currentTokenMatches(STRICT) || p.currentTokenMatches(ENUM_ATTR) || p.currentTokenMatches(FLAGS) {
attrs = p.parseAttributes()
// parseAttributes advances to the declaration token
}
// Check for private modifier
visibility := VisibilityPublic
if p.currentTokenMatches(PRIVATE) {
visibility = VisibilityPrivate
p.nextToken() // move past PRIVATE to the declaration
}
// Validate #suppress is only used on function declarations
if suppressAttr := hasSuppressAttribute(attrs); suppressAttr != nil {
if !p.currentTokenMatches(DO) {
p.addEZError(errors.E2051, "#suppress can only be applied to function declarations", suppressAttr.Token)
// Clear #suppress attributes to prevent them from being applied
newAttrs := []*Attribute{}
for _, attr := range attrs {
if attr.Name != "suppress" {
newAttrs = append(newAttrs, attr)
}
}
attrs = newAttrs
}
}
// Validate #strict is only used on when statements
if strictAttr := hasStrictAttribute(attrs); strictAttr != nil {
if !p.currentTokenMatches(WHEN) {
p.addEZError(errors.E2055, "#strict can only be applied to when statements", strictAttr.Token)
// Clear #strict attributes to prevent them from being applied
newAttrs := []*Attribute{}
for _, attr := range attrs {
if attr.Name != "strict" {
newAttrs = append(newAttrs, attr)
}
}
attrs = newAttrs
}
}
switch p.currentToken.Type {
case CONST:
// For struct declarations like "const Name struct { ... }",
// we handle them in parseVarableDeclaration by detecting the STRUCT token
stmt := p.parseVarableDeclarationOrStruct()
if stmt != nil {
// Apply visibility and attributes
switch s := stmt.(type) {
case *VariableDeclaration:
s.Visibility = visibility
if len(attrs) > 0 {
s.Attributes = attrs
}
case *StructDeclaration:
s.Visibility = visibility
case *EnumDeclaration:
s.Visibility = visibility
if len(attrs) > 0 {
s.Attributes = p.parseEnumAttributes(attrs)
}
}
}
return stmt
case TEMP:
stmt := p.parseVarableDeclaration()
if stmt != nil {
stmt.Visibility = visibility
if len(attrs) > 0 {
stmt.Attributes = attrs
}
}
return stmt
case DO:
stmt := p.parseFunctionDeclarationWithAttrs(attrs)
if stmt != nil {
stmt.Visibility = visibility
}
return stmt
case RETURN:
return p.parseReturnStatement()
case ENSURE:
return p.parseEnsureStatement()
case IF:
return p.parseIfStatement()
case WHEN:
return p.parseWhenStatement(attrs)
case FOR:
return p.parseForStatement()
case FOR_EACH:
return p.parseForEachStatement()
case AS_LONG_AS:
return p.parseWhileStatement()
case LOOP:
return p.parseLoopStatement()
case BREAK:
return p.parseBreakStatement()
case CONTINUE:
return p.parseContinueStatement()
case IMPORT:
return p.parseImportStatement()
case USING:
return p.parseUsingStatement()
case IDENT:
// Check for tuple unpacking assignment: a, b = func() (#699)
if p.peekTokenMatches(COMMA) {
return p.parseTupleAssignment()
}
// Parse the expression first to handle identifiers, index expressions, and member access
expr := p.parseExpression(LOWEST)
// Check if an assignment operator follows
if p.peekTokenMatches(ASSIGN) || p.peekTokenMatches(PLUS_ASSIGN) ||
p.peekTokenMatches(MINUS_ASSIGN) || p.peekTokenMatches(ASTERISK_ASSIGN) ||
p.peekTokenMatches(SLASH_ASSIGN) || p.peekTokenMatches(PERCENT_ASSIGN) {
// Convert to assignment statement
stmt := &AssignmentStatement{Token: p.currentToken}
stmt.Name = expr
// Validate assignment target
switch expr.(type) {
case *Label, *IndexExpression, *MemberExpression:
// Valid assignment targets
default:
msg := fmt.Sprintf("invalid assignment target: cannot assign to %T", expr)
p.addEZError(errors.E2008, msg, p.currentToken)
}
// Get the assignment operator
p.nextToken()
stmt.Operator = p.currentToken.Literal
// Parse the value expression
p.nextToken()
stmt.Value = p.parseExpression(LOWEST)
return stmt
}
// Not an assignment, treat as expression statement
return &ExpressionStatement{Token: p.currentToken, Expression: expr}
default:
return p.parseExpressionStatement()
}
}
// ============================================================================
// Statement Parsers
// ============================================================================
// parseVarableDeclarationOrStruct handles variable declarations, struct declarations,
// and enum declarations that start with "const Name struct/enum { ... }"
func (p *Parser) parseVarableDeclarationOrStruct() Statement {
// Check if this is "const Name struct {...}" or "const Name enum {...}"
if p.currentTokenMatches(CONST) && p.peekTokenMatches(IDENT) {
// Peek ahead one more token to see if it's STRUCT or ENUM
savedCurrent := p.currentToken
p.nextToken() // move to Name
nameToken := p.currentToken
if p.peekTokenMatches(STRUCT) {
// This is a struct declaration
// currentToken is now the struct name, which is what parseStructDeclaration expects
result := p.parseStructDeclaration()
if result == nil {
return nil // Return untyped nil to avoid typed nil interface issue
}
return result
}
if p.peekTokenMatches(ENUM) {
// This is an enum declaration
// currentToken is now the enum name, which is what parseEnumDeclaration expects
result := p.parseEnumDeclaration()
if result == nil {
return nil // Return untyped nil to avoid typed nil interface issue
}
return result
}
// Not a struct, restore and parse as variable
// But we've already consumed one token, so we need to account for that
// The best approach is to just continue with variable parsing from here
// We're at the name token, so we can build the VariableDeclaration
stmt := &VariableDeclaration{Token: savedCurrent}
stmt.Mutable = false // it's a const
// Check for reserved names
if isReservedName(nameToken.Literal) {
msg := fmt.Sprintf("'%s' is a reserved keyword and cannot be used as a variable name", nameToken.Literal)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2020, msg, nameToken)
return nil
}
// Check for duplicate declaration
if !p.declareInScope(nameToken.Literal, nameToken) {
msg := fmt.Sprintf("'%s' is already declared in this scope", nameToken.Literal)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2023, msg, nameToken)
return nil
}
stmt.Names = append(stmt.Names, &Label{Token: nameToken, Value: nameToken.Literal})
stmt.Name = stmt.Names[0]
// Check if this is a type-inferred assignment (const x = value)
// This allows: const p = new(Person) or const p = Person{...}
if p.peekTokenMatches(ASSIGN) {
p.nextToken() // consume =
p.nextToken() // move to value
stmt.Value = p.parseExpression(LOWEST)
return stmt
}
// Parse type using parseTypeName which handles qualified names (module.Type),
// arrays ([type], [type,size]), and maps (map[key:value])
p.nextToken()
stmt.TypeName = p.parseTypeName()
if stmt.TypeName == "" {
return nil
}
// Optional initialization
if p.peekTokenMatches(ASSIGN) {
assignToken := p.peekToken
p.nextToken() // consume =
p.nextToken() // move to value
if p.currentTokenMatches(RBRACE) || p.currentTokenMatches(EOF) {
msg := "expected expression after '='"
p.errors = append(p.errors, msg)
p.addEZError(errors.E2003, msg, assignToken)
return nil
}
stmt.Value = p.parseExpression(LOWEST)
} else if !stmt.Mutable {
// const must be initialized
msg := fmt.Sprintf("const '%s' must be initialized with a value", stmt.Name.Value)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2011, msg, p.currentToken)
return nil
}
return stmt
}
// Not the special "const Name struct" case, use regular variable declaration
result := p.parseVarableDeclaration()
if result == nil {
return nil // Return untyped nil to avoid typed nil interface issue
}
return result
}
func (p *Parser) parseVarableDeclaration() *VariableDeclaration {
stmt := &VariableDeclaration{Token: p.currentToken}
stmt.Mutable = p.currentToken.Type == TEMP
// Check for blank identifier (_) or IDENT
if p.peekTokenMatches(BLANK) {
p.nextToken()
stmt.Names = append(stmt.Names, &Label{Token: p.currentToken, Value: "_"})
} else if IsKeyword(p.peekToken.Type) {
// If user tries to use a keyword as variable name, give helpful error
keyword := KeywordLiteral(p.peekToken.Type)
msg := fmt.Sprintf("'%s' is a reserved keyword and cannot be used as a variable name", keyword)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2020, msg, p.peekToken)
return nil
} else if !p.expectPeek(IDENT) {
return nil
} else {
name := p.currentToken.Literal
// Check for reserved names
if isReservedName(name) {
msg := fmt.Sprintf("'%s' is a reserved keyword and cannot be used as a variable name", name)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2020, msg, p.currentToken)
return nil
}
// Check for duplicate declaration
if !p.declareInScope(name, p.currentToken) {
msg := fmt.Sprintf("'%s' is already declared in this scope", name)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2023, msg, p.currentToken)
return nil
}
stmt.Names = append(stmt.Names, &Label{Token: p.currentToken, Value: name})
}
// Check for multiple assignment: temp result, err = ...
for p.peekTokenMatches(COMMA) {
p.nextToken() // consume comma
p.nextToken() // move to next identifier or _
if p.currentTokenMatches(BLANK) {
stmt.Names = append(stmt.Names, &Label{Token: p.currentToken, Value: "_"})
} else if IsKeyword(p.currentToken.Type) {
// If user tries to use a keyword as variable name, give helpful error
keyword := KeywordLiteral(p.currentToken.Type)
msg := fmt.Sprintf("'%s' is a reserved keyword and cannot be used as a variable name", keyword)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2020, msg, p.currentToken)
return nil
} else if p.currentTokenMatches(IDENT) {
name := p.currentToken.Literal
// Check for reserved names
if isReservedName(name) {
msg := fmt.Sprintf("'%s' is a reserved keyword and cannot be used as a variable name", name)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2020, msg, p.currentToken)
return nil
}
// Check for duplicate declaration
if !p.declareInScope(name, p.currentToken) {
msg := fmt.Sprintf("'%s' is already declared in this scope", name)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2023, msg, p.currentToken)
return nil
}
stmt.Names = append(stmt.Names, &Label{Token: p.currentToken, Value: name})
} else {
msg := fmt.Sprintf("expected identifier or _ (blank identifier), got %s", p.currentToken.Type)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2029, msg, p.currentToken)
return nil
}
}
// If multiple names, expect = directly (type inferred from function return)
if len(stmt.Names) > 1 {
stmt.Name = stmt.Names[0] // keep first name for backward compat
if !p.expectPeek(ASSIGN) {
return nil
}
p.nextToken() // move to value
stmt.Value = p.parseExpression(LOWEST)
return stmt
}
// Single variable declaration - set Name for backward compatibility
stmt.Name = stmt.Names[0]
// Check if this is a type-inferred assignment (temp x = value)
if p.peekTokenMatches(ASSIGN) {
p.nextToken() // consume =
p.nextToken() // move to value
stmt.Value = p.parseExpression(LOWEST)
return stmt
}
// Parse type - can be IDENT or [type] for arrays
p.nextToken()
stmt.TypeName = p.parseTypeName()
if stmt.TypeName == "" {
return nil
}
// Check for typed tuple unpacking: temp a int, b string = getValues()
if p.peekTokenMatches(COMMA) {
// Initialize TypeNames with the first type
stmt.TypeNames = append(stmt.TypeNames, stmt.TypeName)
// Parse additional (name type) pairs
for p.peekTokenMatches(COMMA) {
p.nextToken() // consume comma
p.nextToken() // move to next identifier
if !p.currentTokenMatches(IDENT) {
msg := fmt.Sprintf("expected identifier, got %s", p.currentToken.Type)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2029, msg, p.currentToken)
return nil
}
name := p.currentToken.Literal
// Check for reserved names
if isReservedName(name) {
msg := fmt.Sprintf("'%s' is a reserved keyword and cannot be used as a variable name", name)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2020, msg, p.currentToken)
return nil
}
// Check for duplicate declaration
if !p.declareInScope(name, p.currentToken) {
msg := fmt.Sprintf("'%s' is already declared in this scope", name)
p.errors = append(p.errors, msg)
p.addEZError(errors.E2023, msg, p.currentToken)
return nil
}
stmt.Names = append(stmt.Names, &Label{Token: p.currentToken, Value: name})