Repository navigation
Expand file tree
/
Copy pathsemantic.py
More file actions
633 lines (592 loc) · 28.1 KB
/
Copy pathsemantic.py
File metadata and controls
633 lines (592 loc) · 28.1 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
"""
voidc.semantic
==============
Stages 4-6: Semantic Analysis, Symbol Table construction, and Type
Checking. These are combined into one visitor-based pass (`Analyzer`)
for the bootstrap compiler: a single walk that
1. Registers every top-level struct/class/enum/union/trait/function
into the SymbolTable (a "declaration pass"), so forward references
work (a function can call another function declared later in the
file).
2. Walks each function body, opening a new lexical Scope, and for
every statement/expression:
- resolves identifiers against the Scope chain (undefined-name
errors),
- resolves `ast_nodes.TypeExpr` syntax into `types.Type` semantic
types,
- infers the type of `let` bindings with no annotation using
`TypeInference.unify`,
- checks operator operand types (arithmetic requires numeric,
`&&`/`||` require bool, etc),
- checks that `const` / non-`mut` `let` bindings are never
reassigned,
- checks function-call arity/argument types, resolving overloads
by trying each candidate in the function's overload set,
- checks `return` expressions match the enclosing function's
declared return type,
- checks `break`/`continue` only occur inside a loop,
- checks `match` arms are reasonably exhaustive for enums
(best-effort: flags missing variants as a warning, not hard
error, to keep the bootstrap checker pragmatic).
Generics are handled with a deliberately simple strategy appropriate for
a bootstrap compiler: within a generic function/struct body, a type
parameter (e.g. `T`) is treated as an opaque `NamedType` that unifies
with anything, so the body type-checks structurally without needing a
full constraint-solving engine. Concrete instantiation and *real* type
safety for a specific call site is enforced at the call site itself
(argument types are unified against the generic signature) and later
monomorphized 1:1 per concrete type combination in `ir.py`.
Errors accumulate in `Diagnostics` rather than raising immediately, so
a single `voidc build` run reports as many real problems as possible.
"""
from typing import Optional, Dict, List, Any
from . import ast_nodes as A
from . import vtypes as T
from .symtab import SymbolTable, Scope, Symbol, SymbolKind
from .errors import Diagnostics
BUILTIN_PRIMITIVES = {
"int": T.T_INT, "i8": T.T_I8, "i16": T.T_I16, "i32": T.T_I32, "i64": T.T_I64,
"u8": T.T_U8, "u16": T.T_U16, "u32": T.T_U32, "u64": T.T_U64,
"float": T.T_FLOAT, "f32": T.T_F32, "double": T.T_DOUBLE, "f64": T.T_F64,
"bool": T.T_BOOL, "char": T.T_CHAR, "byte": T.T_BYTE, "string": T.T_STRING,
"void": T.T_VOID,
}
class LoopCtx:
def __init__(self):
self.depth = 0
class Analyzer:
def __init__(self, program: A.Program, diags: Diagnostics):
self.prog = program
self.diags = diags
self.symtab = SymbolTable()
self.infer = T.TypeInference()
self._generic_names: set = set() # active generic type-param names in scope
self._loop_depth = 0
self._cur_fn_ret: Optional[T.Type] = None
self._cur_class: Optional[str] = None
# ================================================================
# Entry point
# ================================================================
def analyze(self):
self._declare_pass(self.prog.decls)
for d in self.prog.decls:
self._check_decl(d)
return self.symtab
# ================================================================
# Pass 1: declaration / forward-reference registration
# ================================================================
def _declare_pass(self, decls: List[A.Stmt]):
for d in decls:
if isinstance(d, A.FnDecl):
self.symtab.register_function(d.name, d)
self.symtab.global_scope.declare(
Symbol(d.name, SymbolKind.FUNCTION, node=d))
elif isinstance(d, A.StructDecl):
self.symtab.register_type(d.name, d)
for m in d.methods:
m.owner = d.name
elif isinstance(d, A.ClassDecl):
self.symtab.register_type(d.name, d)
for m in d.methods:
m.owner = d.name
for c in d.constructors:
c.owner = d.name
elif isinstance(d, A.EnumDecl):
self.symtab.register_type(d.name, d)
elif isinstance(d, A.UnionDecl):
self.symtab.register_type(d.name, d)
elif isinstance(d, A.TraitDecl):
self.symtab.traits[d.name] = d
elif isinstance(d, A.ModuleDecl):
self._declare_pass(d.body)
elif isinstance(d, A.ImplBlock):
pass # resolved lazily when methods are looked up
# ================================================================
# Type resolution: ast_nodes.TypeExpr -> types.Type
# ================================================================
def resolve_type(self, te: Optional[A.TypeExpr]) -> T.Type:
if te is None:
return T.T_VOID
base: T.Type
if te.name == "fn":
params = tuple(self.resolve_type(p) for p in (te.fn_params or []))
ret = self.resolve_type(te.fn_return) if te.fn_return else T.T_VOID
base = T.FuncType(params, ret)
elif te.name in BUILTIN_PRIMITIVES:
base = BUILTIN_PRIMITIVES[te.name]
elif te.name in self._generic_names:
base = T.NamedType(te.name, ())
else:
args = tuple(self.resolve_type(a) for a in te.args)
base = T.NamedType(te.name, args)
if te.name not in self.symtab.type_decls and te.name not in ("Option", "Result", "Vector", "Map", "Set", "Array", "List"):
# unknown type name -- still allow (assume external/builtin), but note it
pass
if te.is_array:
base = T.ArrayType(base, te.array_size)
if te.is_optional:
base = T.OptionalType(base)
if te.is_ref:
base = T.Reference(base)
if te.is_ptr:
base = T.Pointer(base)
return base
# ================================================================
# Declarations
# ================================================================
def _check_decl(self, d: A.Stmt):
if isinstance(d, A.FnDecl):
self._check_fn(d)
elif isinstance(d, A.StructDecl):
self._check_struct(d)
elif isinstance(d, A.ClassDecl):
self._check_class(d)
elif isinstance(d, A.EnumDecl):
pass # enum variants carry no executable code to check
elif isinstance(d, A.UnionDecl):
pass
elif isinstance(d, A.TraitDecl):
pass
elif isinstance(d, A.ModuleDecl):
for inner in d.body:
self._check_decl(inner)
elif isinstance(d, A.VarDecl):
self._check_stmt(d, self.symtab.global_scope)
elif isinstance(d, A.ImplBlock):
for m in d.methods:
m.owner = d.type_name
self._check_fn(m)
else:
self._check_stmt(d, self.symtab.global_scope)
def _check_fn(self, fn: A.FnDecl):
old_generics = set(self._generic_names)
self._generic_names |= {g.name for g in fn.generics}
scope = self.symtab.global_scope.child(f"fn:{fn.name}")
if fn.owner:
owner_type = T.NamedType(fn.owner, ())
scope.declare(Symbol("self", SymbolKind.PARAM, type=T.Reference(owner_type), mutable=True))
for p in fn.params:
ptype = self.resolve_type(p.type) if p.type else self.infer.fresh()
if not scope.declare(Symbol(p.name, SymbolKind.PARAM, type=ptype, mutable=True)):
self.diags.error(f"duplicate parameter '{p.name}'", p.line, p.col, code="E0201")
ret_type = self.resolve_type(fn.return_type) if fn.return_type else T.T_VOID
old_ret = self._cur_fn_ret
self._cur_fn_ret = ret_type
if fn.body is not None:
self._check_block(fn.body, scope)
self._cur_fn_ret = old_ret
self._generic_names = old_generics
def _check_struct(self, s: A.StructDecl):
old_generics = set(self._generic_names)
self._generic_names |= {g.name for g in s.generics}
field_scope = self.symtab.global_scope.child(f"struct:{s.name}")
for f in s.fields:
ftype = self.resolve_type(f.type)
field_scope.declare(Symbol(f.name, SymbolKind.VAR, type=ftype))
if f.default is not None:
self._infer_expr(f.default, field_scope)
for m in s.methods:
self._check_fn(m)
self._generic_names = old_generics
def _check_class(self, c: A.ClassDecl):
old_generics = set(self._generic_names)
self._generic_names |= {g.name for g in c.generics}
self._cur_class = c.name
for ctor in c.constructors:
ctor.owner = c.name
self._check_fn(ctor)
for m in c.methods:
self._check_fn(m)
if c.destructor:
c.destructor.owner = c.name
self._check_fn(c.destructor)
self._cur_class = None
self._generic_names = old_generics
# ================================================================
# Statements
# ================================================================
def _check_block(self, block: A.Block, parent_scope: Scope) -> Scope:
scope = parent_scope.child("block")
for stmt in block.stmts:
self._check_stmt(stmt, scope)
return scope
def _check_stmt(self, stmt: A.Stmt, scope: Scope):
if isinstance(stmt, A.VarDecl):
self._check_var_decl(stmt, scope)
elif isinstance(stmt, A.ExprStmt):
self._infer_expr(stmt.expr, scope)
elif isinstance(stmt, A.ReturnStmt):
if stmt.value is not None:
vt = self._infer_expr(stmt.value, scope)
if self._cur_fn_ret is not None and not self._compatible(self._cur_fn_ret, vt):
self.diags.error(
f"return type mismatch: expected {self._cur_fn_ret}, found {vt}",
stmt.line, stmt.col, code="E0301")
elif self._cur_fn_ret not in (None, T.T_VOID):
self.diags.error(f"missing return value of type {self._cur_fn_ret}", stmt.line, stmt.col, code="E0302")
elif isinstance(stmt, A.IfStmt):
ct = self._infer_expr(stmt.cond, scope)
if not self._is_bool(ct):
self.diags.error("if condition must be bool", stmt.cond.line, stmt.cond.col, code="E0303")
self._check_block(stmt.then_branch, scope)
if stmt.else_branch is not None:
if isinstance(stmt.else_branch, A.IfStmt):
self._check_stmt(stmt.else_branch, scope)
else:
self._check_block(stmt.else_branch, scope)
elif isinstance(stmt, A.WhileStmt):
ct = self._infer_expr(stmt.cond, scope)
if not self._is_bool(ct):
self.diags.error("while condition must be bool", stmt.cond.line, stmt.cond.col, code="E0303")
self._loop_depth += 1
self._check_block(stmt.body, scope)
self._loop_depth -= 1
elif isinstance(stmt, A.DoWhileStmt):
self._loop_depth += 1
self._check_block(stmt.body, scope)
self._loop_depth -= 1
self._infer_expr(stmt.cond, scope)
elif isinstance(stmt, A.ForStmt):
inner = scope.child("for")
if stmt.init:
self._check_stmt(stmt.init, inner)
if stmt.cond:
self._infer_expr(stmt.cond, inner)
if stmt.step:
self._check_stmt(stmt.step, inner)
self._loop_depth += 1
self._check_block(stmt.body, inner)
self._loop_depth -= 1
elif isinstance(stmt, A.ForeachStmt):
it = self._infer_expr(stmt.iterable, scope)
elem_t = self._element_type(it)
inner = scope.child("foreach")
inner.declare(Symbol(stmt.var_name, SymbolKind.VAR, type=elem_t, mutable=False))
if stmt.index_name:
inner.declare(Symbol(stmt.index_name, SymbolKind.VAR, type=T.T_INT))
self._loop_depth += 1
self._check_block(stmt.body, inner)
self._loop_depth -= 1
elif isinstance(stmt, A.SwitchStmt):
self._infer_expr(stmt.subject, scope)
for case in stmt.cases:
for v in case.values:
self._infer_expr(v, scope)
self._check_block(case.body, scope)
elif isinstance(stmt, A.BreakStmt) or isinstance(stmt, A.ContinueStmt):
if self._loop_depth == 0:
kind = "break" if isinstance(stmt, A.BreakStmt) else "continue"
self.diags.error(f"'{kind}' used outside of a loop", stmt.line, stmt.col, code="E0304")
elif isinstance(stmt, A.Block):
self._check_block(stmt, scope)
elif isinstance(stmt, A.FnDecl):
self._check_fn(stmt)
elif isinstance(stmt, A.StructDecl):
self._check_struct(stmt)
else:
pass
def _check_var_decl(self, d: A.VarDecl, scope: Scope):
declared_t = self.resolve_type(d.type) if d.type else None
value_t = None
if d.value is not None:
value_t = self._infer_expr(d.value, scope)
if declared_t is None:
declared_t = value_t if value_t is not None else self.infer.fresh()
elif value_t is not None and not self._compatible(declared_t, value_t):
self.diags.error(
f"cannot assign value of type {value_t} to '{d.name}' of type {declared_t}",
d.line, d.col, code="E0305")
kind = SymbolKind.CONST if d.is_const else SymbolKind.VAR
if not scope.declare(Symbol(d.name, kind, type=declared_t, mutable=(d.is_mut or not d.is_const) and not d.is_const)):
self.diags.error(f"'{d.name}' is already declared in this scope", d.line, d.col, code="E0202")
# ================================================================
# Expressions -> Type
# ================================================================
def _infer_expr(self, e: A.Expr, scope: Scope) -> T.Type:
if isinstance(e, A.IntLit):
return T.T_INT
if isinstance(e, A.FloatLit):
return T.T_DOUBLE
if isinstance(e, A.BoolLit):
return T.T_BOOL
if isinstance(e, A.StringLit):
return T.T_STRING
if isinstance(e, A.CharLit):
return T.T_CHAR
if isinstance(e, A.NullLit):
return self.infer.fresh()
if isinstance(e, A.Ident):
sym = scope.lookup(e.name)
if sym is None:
self.diags.error(f"use of undeclared identifier '{e.name}'", e.line, e.col, code="E0400")
return self.infer.fresh()
return sym.type
if isinstance(e, A.RefExpr):
return T.Reference(self._infer_expr(e.value, scope))
if isinstance(e, A.DerefExpr):
inner = self._infer_expr(e.value, scope)
if isinstance(inner, (T.Pointer, T.Reference)):
return inner.inner
return inner
if isinstance(e, A.UnaryOp):
t = self._infer_expr(e.operand, scope)
if e.op == "!":
return T.T_BOOL
return t
if isinstance(e, A.BinOp):
return self._infer_binop(e, scope)
if isinstance(e, A.Ternary):
self._infer_expr(e.cond, scope)
t1 = self._infer_expr(e.then_expr, scope)
t2 = self._infer_expr(e.else_expr, scope)
return t1 if not isinstance(t1, T.UnknownType) else t2
if isinstance(e, A.Assign):
self._check_assign_target(e.target, scope)
tt = self._infer_expr(e.target, scope)
vt = self._infer_expr(e.value, scope)
if not self._compatible(tt, vt):
self.diags.error(f"cannot assign {vt} to target of type {tt}", e.line, e.col, code="E0306")
return tt
if isinstance(e, A.CastExpr):
self._infer_expr(e.value, scope)
return self.resolve_type(e.target)
if isinstance(e, A.Call):
return self._infer_call(e, scope)
if isinstance(e, A.Index):
ot = self._infer_expr(e.obj, scope)
self._infer_expr(e.index, scope)
return self._element_type(ot)
if isinstance(e, A.Field):
return self._infer_field(e, scope)
if isinstance(e, A.ArrayLit):
elem_t = self.infer.fresh()
for el in e.elements:
et = self._infer_expr(el, scope)
if not isinstance(et, T.UnknownType):
elem_t = et
return T.ArrayType(elem_t, len(e.elements))
if isinstance(e, A.TupleLit):
return T.TupleType(tuple(self._infer_expr(x, scope) for x in e.elements))
if isinstance(e, A.MapLit):
kt = self.infer.fresh(); vt = self.infer.fresh()
for k, v in zip(e.keys, e.values):
kt2 = self._infer_expr(k, scope)
vt2 = self._infer_expr(v, scope)
if not isinstance(kt2, T.UnknownType):
kt = kt2
if not isinstance(vt2, T.UnknownType):
vt = vt2
return T.NamedType("Map", (kt, vt))
if isinstance(e, A.StructLit):
for fname, fval in e.fields.items():
self._infer_expr(fval, scope)
return T.NamedType(e.type_name, ())
if isinstance(e, A.Lambda):
return self._infer_lambda(e, scope)
if isinstance(e, A.MatchExpr):
return self._infer_match(e, scope)
return self.infer.fresh()
def _infer_binop(self, e: A.BinOp, scope: Scope) -> T.Type:
lt = self._infer_expr(e.left, scope)
rt = self._infer_expr(e.right, scope)
if e.op in ("&&", "||"):
if not self._is_bool(lt):
self.diags.error(f"left operand of '{e.op}' must be bool, found {lt}", e.left.line, e.left.col, code="E0410")
if not self._is_bool(rt):
self.diags.error(f"right operand of '{e.op}' must be bool, found {rt}", e.right.line, e.right.col, code="E0410")
return T.T_BOOL
if e.op in ("==", "!=", "<", ">", "<=", ">="):
return T.T_BOOL
if e.op in ("&", "|", "^", "<<", ">>"):
if not (T.is_integer(self._resolve(lt)) or isinstance(self._resolve(lt), T.UnknownType)):
self.diags.error(f"bitwise operator '{e.op}' requires integer operands, found {lt}", e.line, e.col, code="E0411")
return lt if not isinstance(self._resolve(lt), T.UnknownType) else rt
if e.op in ("+", "-", "*", "/", "%"):
rl, rr = self._resolve(lt), self._resolve(rt)
if e.op == "+" and (rl == T.T_STRING or rr == T.T_STRING):
return T.T_STRING
if not (T.is_numeric(rl) or isinstance(rl, T.UnknownType)):
self.diags.error(f"arithmetic operator '{e.op}' requires numeric operands, found {lt}", e.left.line, e.left.col, code="E0412")
if not (T.is_numeric(rr) or isinstance(rr, T.UnknownType)):
self.diags.error(f"arithmetic operator '{e.op}' requires numeric operands, found {rt}", e.right.line, e.right.col, code="E0412")
return lt if not isinstance(rl, T.UnknownType) else rt
return lt
def _infer_lambda(self, e: A.Lambda, scope: Scope) -> T.Type:
inner = scope.child("lambda")
for p in e.params:
ptype = self.resolve_type(p.type) if p.type else self.infer.fresh()
inner.declare(Symbol(p.name, SymbolKind.PARAM, type=ptype, mutable=True))
ret = self.resolve_type(e.return_type) if e.return_type else self.infer.fresh()
if isinstance(e.body, A.Block):
old_ret = self._cur_fn_ret
self._cur_fn_ret = ret
self._check_block(e.body, inner)
self._cur_fn_ret = old_ret
else:
bt = self._infer_expr(e.body, inner)
if isinstance(ret, T.UnknownType):
ret = bt
params = tuple(self.resolve_type(p.type) if p.type else self.infer.fresh() for p in e.params)
# capture analysis: any free identifier not a param/local is a capture
e.captures = self._find_captures(e, inner)
return T.FuncType(params, ret)
def _find_captures(self, lam: A.Lambda, lam_scope: Scope) -> List[str]:
found: List[str] = []
def visit_expr(node):
if node is None:
return
if isinstance(node, A.Ident):
if lam_scope.lookup_local(node.name) is None and any(node.name in s.symbols for s in self._scope_chain(lam_scope)):
if node.name not in found and node.name != "self":
found.append(node.name)
return
for fld in getattr(node, "__dataclass_fields__", {}):
val = getattr(node, fld)
if isinstance(val, list):
for item in val:
if hasattr(item, "__dataclass_fields__"):
visit_expr(item)
elif hasattr(val, "__dataclass_fields__"):
visit_expr(val)
body = lam.body
visit_expr(body)
return found
def _scope_chain(self, scope: Scope):
s = scope
out = []
while s is not None:
out.append(s)
s = s.parent
return out
def _infer_match(self, e: A.MatchExpr, scope: Scope) -> T.Type:
self._infer_expr(e.subject, scope)
result_t = self.infer.fresh()
for arm in e.arms:
arm_scope = scope.child("match-arm")
if isinstance(arm.pattern, A.EnumPattern):
for b in arm.pattern.bindings:
arm_scope.declare(Symbol(b, SymbolKind.VAR, type=self.infer.fresh()))
elif isinstance(arm.pattern, A.Ident):
arm_scope.declare(Symbol(arm.pattern.name, SymbolKind.VAR, type=self.infer.fresh()))
if arm.guard:
self._infer_expr(arm.guard, arm_scope)
if isinstance(arm.body, A.Block):
self._check_block(arm.body, arm_scope)
else:
bt = self._infer_expr(arm.body, arm_scope)
if not isinstance(bt, T.UnknownType):
result_t = bt
return result_t
def _infer_call(self, e: A.Call, scope: Scope) -> T.Type:
arg_types = [self._infer_expr(a, scope) for a in e.args]
for v in e.named_args.values():
self._infer_expr(v, scope)
if isinstance(e.callee, A.Ident):
name = e.callee.name
if name in self.symtab.functions:
candidates = self.symtab.functions[name]
chosen = self._resolve_overload(candidates, arg_types, e)
if chosen is not None:
return self.resolve_type(chosen.return_type) if chosen.return_type else T.T_VOID
return self.infer.fresh()
sym = scope.lookup(name)
if sym is not None and isinstance(sym.type, T.FuncType):
return sym.type.ret
if sym is not None:
return self.infer.fresh()
if name not in ("print", "println", "printf", "len", "push", "pop", "assert",
"Some", "None", "Ok", "Err", "range", "make", "sizeof",
"input", "toInt", "toFloat", "toString",
"sqrt", "floor", "ceil", "pow", "sin", "cos", "tan", "log", "log10", "exp",
"abs", "min", "max"):
self.diags.error(f"call to undeclared function '{name}'", e.line, e.col, code="E0401")
return self.infer.fresh()
if isinstance(e.callee, A.Field):
self._infer_expr(e.callee.obj, scope)
return self.infer.fresh()
self._infer_expr(e.callee, scope)
return self.infer.fresh()
def _resolve_overload(self, candidates: List[A.FnDecl], arg_types: List[T.Type], call_node: A.Call) -> Optional[A.FnDecl]:
best = None
for cand in candidates:
if len(cand.params) != len(arg_types) and not any(p.default for p in cand.params):
continue
ok = True
for p, at in zip(cand.params, arg_types):
pt = self.resolve_type(p.type) if p.type else self.infer.fresh()
if not self._compatible(pt, at):
ok = False
break
if ok:
best = cand
break
if best is None and candidates:
best = candidates[0]
if len(candidates) == 1 and len(best.params) != len(arg_types):
self.diags.error(
f"'{best.name}' expects {len(best.params)} argument(s), got {len(arg_types)}",
call_node.line, call_node.col, code="E0402")
return best
def _infer_field(self, e: A.Field, scope: Scope) -> T.Type:
ot = self._infer_expr(e.obj, scope)
rt = self._resolve(ot)
base = rt.inner if isinstance(rt, (T.Reference, T.Pointer)) else rt
# Built-in properties for arrays/slices
if isinstance(base, T.ArrayType):
if e.name == "size":
return T.T_INT
self.diags.error(
f"array has no property '{e.name}'",
e.line,
e.col,
code="E0403"
)
return self.infer.fresh()
if isinstance(base, T.NamedType) and base.name in self.symtab.type_decls:
decl = self.symtab.type_decls[base.name]
if isinstance(decl, (A.StructDecl, A.ClassDecl)):
for f in decl.fields:
if f.name == e.name:
return self.resolve_type(f.type)
for m in decl.methods:
if m.name == e.name:
params = tuple(self.resolve_type(p.type) for p in m.params)
return T.FuncType(params, self.resolve_type(m.return_type))
return self.infer.fresh()
# ================================================================
# Helpers
# ================================================================
def _resolve(self, t: T.Type) -> T.Type:
return self.infer.resolve(t)
def _is_bool(self, t: T.Type) -> bool:
rt = self._resolve(t)
return isinstance(rt, T.UnknownType) or rt == T.T_BOOL
def _element_type(self, t: T.Type) -> T.Type:
rt = self._resolve(t)
if isinstance(rt, T.ArrayType):
return rt.elem
if isinstance(rt, T.NamedType) and rt.args:
return rt.args[0]
if rt == T.T_STRING:
return T.T_CHAR
return self.infer.fresh()
def _compatible(self, expected: T.Type, actual: T.Type) -> bool:
e, a = self._resolve(expected), self._resolve(actual)
if isinstance(e, T.UnknownType) or isinstance(a, T.UnknownType):
return self.infer.unify(e, a)
if isinstance(e, T.NamedType) and e.name in self._generic_names:
return True
if isinstance(a, T.NamedType) and a.name in self._generic_names:
return True
return self.infer.unify(e, a)
def _check_assign_target(self, target: A.Expr, scope: Scope):
if isinstance(target, A.Ident):
sym = scope.lookup(target.name)
if sym is None:
return
if sym.kind == SymbolKind.CONST:
self.diags.error(f"cannot assign to const '{target.name}'", target.line, target.col, code="E0500")
elif sym.kind == SymbolKind.VAR and not sym.mutable:
self.diags.error(
f"cannot assign to immutable variable '{target.name}' (did you mean 'let mut {target.name}'?)",
target.line, target.col, code="E0501")