Skip to content

Commit 8c8634e

Browse files
authored
Merge pull request #3 from EmberLang/feature/ast-type-side-table
refactor: integrate ExprTypes side table into context for persistent …
2 parents 492ce23 + a2a80eb commit 8c8634e

6 files changed

Lines changed: 99 additions & 74 deletions

File tree

COMPILER_GUIDELINES.md

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The goal is not to copy Rust or any other compiler blindly. The goal is to build
1313

1414
Do not cargo-cult architecture.
1515

16-
When reusing ideas from Rust or any other compiler:
16+
When reusing ideas from Rust, Zig or any other compiler:
1717

1818
- copy the idea only if it fits this language
1919
- simplify when the full design is not needed yet
@@ -58,10 +58,9 @@ These decisions are already part of the language design and should not drift acc
5858
- Zig-style literals are used: `.{ ... }`
5959
- methods are declared outside types using attached-method syntax with receivers.
6060
- `defer` and `panic` are part of the core control-flow model
61-
- builtin functions are declared in `ember_libs_dev/global.em`
62-
- stdlib source modules are declared in `ember_libs_dev/std/*.em`
63-
- builtin declarations use `#[builtin]` and may omit a body
64-
- external declarations use `#[extern(\"...\")]` and may omit a body
61+
- builtin functions are declared in `_builtin_library/global.em`
62+
- stdlib source modules are declared in `_builtin_library/std/*.em`
63+
- external declarations use `#[extern(\"...\")]` and may omit a body. extern can contain the external linking function name as parameter or keep empty for default behavior.
6564
- error unions are explicit value-level control flow and are not exceptions
6665

6766
If implementation changes conflict with this, update the language spec first.
@@ -77,10 +76,6 @@ The compiler should be split into clear layers.
7776
- `hir`: all HIR data structures and HIR-local transforms
7877
- `mir`: MIR and MIR-local transforms
7978
- `semantics`: name resolution, type checking, ownership checks
80-
- `layout`: physical type layout, alignment, size, and field-slot mapping
81-
- `cfg`: CFG data model only
82-
- `cfganalysis`: CFG construction and CFG-based analyses
83-
- `codegen`: lowering and backend work
8479

8580
No package should mix all of these concerns.
8681

@@ -90,27 +85,7 @@ HIR-specific rule:
9085
- keep HIR-related code under `hir`
9186
- if HIR grows, prefer subpackages under `hir/...` over creating parallel top-level `hir*` packages again
9287

93-
## 5. Lexer Rules
94-
95-
The lexer is not a parser.
96-
97-
- tokenize only
98-
- no semantic decisions
99-
- no type inference
100-
- no ownership analysis
101-
- no parser-level hacks
102-
103-
Use regex/token-pattern driven tokenization when it improves clarity.
104-
Do not use regex just because another compiler did. Use it only when the token class is naturally pattern-based, such as numeric literals.
105-
106-
When adding token support:
107-
108-
- keep token names general enough for later phases
109-
- prefer `NUMBER` over fake subcategories unless syntax requires the split
110-
- keep literals normalized only when that is clearly beneficial
111-
- do not silently erase information needed by later phases
112-
113-
## 6. Parser Rules
88+
## 5. Parser Rules
11489

11590
The parser should build syntax, not interpretation.
11691

@@ -127,7 +102,7 @@ Parser code should answer:
127102

128103
If a function makes that hard to see, rewrite it.
129104

130-
## 7. AST Rules
105+
## 6. AST Rules
131106

132107
AST nodes represent source structure, not semantic conclusions.
133108

@@ -140,7 +115,7 @@ Examples:
140115
- `NumberLit` is better than `IntLit` if the lexer accepts non-integer numerics
141116
- `ImportDecl` should exist if imports are part of module syntax
142117

143-
## 8. Context And Pipeline Rules
118+
## 7. Context And Pipeline Rules
144119

145120
Use a central compiler context for shared state.
146121

@@ -175,7 +150,7 @@ The pipeline owns:
175150

176151
Do not hide pipeline behavior inside parser or lexer code.
177152

178-
## 8.1 Phase Responsibilities
153+
## 7.1 Phase Responsibilities
179154

180155
Phase ownership must stay explicit.
181156

@@ -255,7 +230,7 @@ Reason:
255230
- it interacts with control flow, reinitialization, and escapes
256231
- keeping it separate makes the typechecker simpler and keeps ownership logic aligned with later CFG/data-flow work
257232

258-
## 8.2 Unwind And Error Model
233+
## 7.2 Unwind And Error Model
259234

260235
Do not conflate `panic` with `E!T`.
261236

@@ -273,7 +248,7 @@ This implies:
273248

274249
Do not fake panic semantics by lowering it to an ordinary call and hoping codegen reconstructs unwind behavior later.
275250

276-
## 8.3 Semantic Order vs Physical Layout
251+
## 7.3 Semantic Order vs Physical Layout
277252

278253
Semantic field order and physical field layout are different concepts.
279254

internal/analysis/semantics/collector/collector.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func (c *collector) collectModule(mod *ast.Module) {
2020
return
2121
}
2222
c.module.ModuleScope = table.New(c.ctx.GlobalScope)
23-
c.module.ResetSemantics()
23+
c.module.ResetSemanticData()
2424
for alias := range c.module.Imports {
2525
if alias == "" {
2626
continue

internal/analysis/semantics/ownership/checker.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ func (c *checker) exprType(scope *table.Scope, expr ast.Expr) symbols.Type {
252252
if c == nil || scope == nil || expr == nil {
253253
return nil
254254
}
255+
if c.module != nil && c.module.ExprTypes != nil {
256+
if t, ok := c.module.ExprTypes[expr]; ok && t != nil {
257+
return t
258+
}
259+
}
255260
switch node := expr.(type) {
256261
case *ast.Ident:
257262
sym, ok := scope.Lookup(node.Name)

internal/analysis/semantics/typechecker/typechecker.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,20 @@ func (c *checker) checkFunctionShape(decl *ast.FnDecl) {
274274
}
275275
}
276276

277-
// typeExpr computes the type of an expression using scope lookup and returns it.
278-
// The typechecker no longer builds a parallel typeinfo.Expr tree — types are
279-
// stored directly on symbols. HIR-lower will re-derive types during lowering.
280-
func (c *checker) typeExpr(scope *table.Scope, expr ast.Expr, expected typeinfo.Type) typeinfo.Type {
277+
// typeExpr computes the type of an expression using scope lookup, records it in the
278+
// module's ExprTypes side table for downstream phases, and returns it.
279+
func (c *checker) typeExpr(scope *table.Scope, expr ast.Expr, expected typeinfo.Type) (resolved typeinfo.Type) {
281280
if expr == nil {
282281
return nil
283282
}
283+
defer func() {
284+
if resolved != nil && c.module != nil {
285+
if c.module.ExprTypes == nil {
286+
c.module.ExprTypes = make(map[ast.Expr]typeinfo.Type)
287+
}
288+
c.module.ExprTypes[expr] = resolved
289+
}
290+
}()
284291
switch node := expr.(type) {
285292
case *ast.NumberLit:
286293
return c.typeNumber(node, expected)

internal/context/context.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ type Module struct {
7272
ModuleScope *table.Scope
7373
// Mappings from block statements to resolved block scopes.
7474
BlockScopes map[*ast.BlockStmt]*table.Scope
75+
// Mappings from expressions to resolved types.
76+
ExprTypes map[ast.Expr]typeinfo.Type
7577
// Import alias -> resolved module import.
7678
Imports map[string]ResolvedImport
7779

@@ -221,9 +223,10 @@ func declarePredeclaredConst(scope *table.Scope, name string) {
221223
}
222224
}
223225

224-
func (m *Module) ResetSemantics() {
226+
func (m *Module) ResetSemanticData() {
225227
if m == nil {
226228
return
227229
}
228230
m.BlockScopes = make(map[*ast.BlockStmt]*table.Scope)
231+
m.ExprTypes = make(map[ast.Expr]typeinfo.Type)
229232
}

internal/ir/hir_lower/lower.go

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -213,79 +213,114 @@ func lowerElse(module *context.Module, scope *table.Scope, stmt ast.Stmt, return
213213
}
214214

215215
// lowerASTExpr directly lowers an AST expression to an IR expression using
216-
// scope lookup for symbol resolution and expectedType for literal coercion.
216+
// the module context's resolved expression types side-table.
217217
func lowerASTExpr(ctx *context.CompilerContext, module *context.Module, scope *table.Scope, expr ast.Expr, expectedType string) ir.Expr {
218218
if expr == nil {
219219
return &ir.InvalidExpr{Message: "nil expression", Type: "<invalid>"}
220220
}
221+
222+
// Fetch canonical type from the typechecker side-table when available.
223+
resolvedTypeStr := ""
224+
if module != nil && module.ExprTypes != nil {
225+
if t, ok := module.ExprTypes[expr]; ok && t != nil {
226+
resolvedTypeStr = typeinfo.TypeText(t)
227+
}
228+
}
229+
221230
switch node := expr.(type) {
222231
case *ast.NumberLit:
223-
return lowerNumberLit(node, expectedType)
232+
t := resolvedTypeStr
233+
if t == "" {
234+
t = expectedType
235+
}
236+
return lowerNumberLit(node, t)
224237

225238
case *ast.StringLit:
226-
return &ir.StringLit{Value: node.Value, Type: "cstr"}
239+
t := resolvedTypeStr
240+
if t == "" || t == "<invalid>" {
241+
t = "cstr"
242+
}
243+
return &ir.StringLit{Value: node.Value, Type: t}
227244

228245
case *ast.Ident:
229246
sym, ok := scope.Lookup(node.Name)
230247
if !ok || sym == nil {
231248
return &ir.InvalidExpr{Message: "unresolved identifier: " + node.Name, Type: "<invalid>"}
232249
}
233-
return &ir.Ident{Name: symbolName(sym), Type: symTypeText(sym)}
250+
t := resolvedTypeStr
251+
if t == "" || t == "<invalid>" || t == "<unknown>" {
252+
t = symTypeText(sym)
253+
}
254+
return &ir.Ident{Name: symbolName(sym), Type: t}
234255

235256
case *ast.ScopeResolution:
236257
if sym := lookupScopeResolutionSymbol(ctx, module, scope, node); sym != nil {
237-
return &ir.Ident{Name: symbolName(sym), Type: symTypeText(sym)}
258+
t := resolvedTypeStr
259+
if t == "" || t == "<invalid>" || t == "<unknown>" {
260+
t = symTypeText(sym)
261+
}
262+
return &ir.Ident{Name: symbolName(sym), Type: t}
238263
}
239264
return &ir.InvalidExpr{Message: "unresolved qualified identifier: " + node.Module.Name + "::" + node.Name.Name, Type: "<invalid>"}
240265

241266
case *ast.UnaryExpr:
242267
arg := lowerASTExpr(ctx, module, scope, node.Expr, expectedType)
243-
exprType := arg.TypeText()
244-
if node.Op == "!" {
245-
exprType = "bool"
268+
t := resolvedTypeStr
269+
if t == "" || t == "<invalid>" {
270+
t = arg.TypeText()
271+
if node.Op == "!" {
272+
t = "bool"
273+
}
246274
}
247-
return &ir.Unary{Op: node.Op, Arg: arg, Type: exprType}
275+
return &ir.Unary{Op: node.Op, Arg: arg, Type: t}
248276

249277
case *ast.BinaryExpr:
250278
left := lowerASTExpr(ctx, module, scope, node.Left, expectedType)
251279
right := lowerASTExpr(ctx, module, scope, node.Right, expectedType)
252-
exprType := left.TypeText()
253-
switch node.Op {
254-
case "==", "!=", "<", "<=", ">", ">=", "&&", "||":
255-
exprType = "bool"
280+
t := resolvedTypeStr
281+
if t == "" || t == "<invalid>" {
282+
t = left.TypeText()
283+
switch node.Op {
284+
case "==", "!=", "<", "<=", ">", ">=", "&&", "||":
285+
t = "bool"
286+
}
256287
}
257-
return &ir.Binary{Op: node.Op, Left: left, Right: right, Type: exprType}
288+
return &ir.Binary{Op: node.Op, Left: left, Right: right, Type: t}
258289

259290
case *ast.CallExpr:
260291
calleeExpr := lowerASTExpr(ctx, module, scope, node.Callee, "")
261292
args := make([]ir.Expr, 0, len(node.Args))
262293
for _, arg := range node.Args {
263294
args = append(args, lowerASTExpr(ctx, module, scope, arg, ""))
264295
}
265-
// Get the return type from the callee symbol (handles qualified callees too).
266-
retType := "<invalid>"
267-
var sym *symbols.Symbol
268-
switch callee := node.Callee.(type) {
269-
case *ast.Ident:
270-
if s, ok := scope.Lookup(callee.Name); ok {
271-
sym = s
272-
}
273-
case *ast.ScopeResolution:
274-
if s := lookupScopeResolutionSymbol(ctx, module, scope, callee); s != nil {
275-
sym = s
296+
t := resolvedTypeStr
297+
if t == "" || t == "<invalid>" {
298+
var sym *symbols.Symbol
299+
switch callee := node.Callee.(type) {
300+
case *ast.Ident:
301+
if s, ok := scope.Lookup(callee.Name); ok {
302+
sym = s
303+
}
304+
case *ast.ScopeResolution:
305+
if s := lookupScopeResolutionSymbol(ctx, module, scope, callee); s != nil {
306+
sym = s
307+
}
276308
}
277-
}
278-
if sym != nil {
279-
if fnType, ok := sym.Type.(*typeinfo.FuncType); ok && fnType != nil && fnType.Return != nil {
280-
retType = typeinfo.TypeText(fnType.Return)
309+
if sym != nil {
310+
if fnType, ok := sym.Type.(*typeinfo.FuncType); ok && fnType != nil && fnType.Return != nil {
311+
t = typeinfo.TypeText(fnType.Return)
312+
}
281313
}
282314
}
283-
return &ir.Call{Callee: calleeExpr, Args: args, Type: retType}
315+
return &ir.Call{Callee: calleeExpr, Args: args, Type: t}
284316

285317
case *ast.AsExpr:
286-
targetTypeStr := typeinfo.TypeText(typeinfo.TypeFromSyntax(node.TypeExpr))
287-
subExpr := lowerASTExpr(ctx, module, scope, node.Expr, targetTypeStr)
288-
return &ir.Cast{Expr: subExpr, Type: targetTypeStr}
318+
t := resolvedTypeStr
319+
if t == "" || t == "<invalid>" {
320+
t = typeinfo.TypeText(typeinfo.TypeFromSyntax(node.TypeExpr))
321+
}
322+
subExpr := lowerASTExpr(ctx, module, scope, node.Expr, t)
323+
return &ir.Cast{Expr: subExpr, Type: t}
289324

290325
default:
291326
return &ir.InvalidExpr{Message: "unsupported expression", Type: "<invalid>"}

0 commit comments

Comments
 (0)