broaden type inference from context (first pass)
This commit is contained in:
@@ -198,9 +198,40 @@
|
|||||||
a `float` param accepts an integer-literal argument as f64 (e.g. `f(3)`)
|
a `float` param accepts an integer-literal argument as f64 (e.g. `f(3)`)
|
||||||
- a function result is narrowed to the constraint's family
|
- a function result is narrowed to the constraint's family
|
||||||
|
|
||||||
14. broaden type inference to surrounding context
|
14. broaden type inference to surrounding context (implemented)
|
||||||
|
- a slot's concrete type is the join of demands reachable from its declaration,
|
||||||
|
flowing backward as well as forward to a fixpoint (the existing global/spec
|
||||||
|
fixpoint in `infer_all`), generalizing milestone 13's forward-only resolution
|
||||||
|
- an "open constant" (a global or local with no concrete annotation plus a compile-time
|
||||||
|
integer initializer) is sign-agnostic until used: a backward demand from any reachable
|
||||||
|
use picks its family/width as long as the value fits, so `A :: 10` followed by
|
||||||
|
`B u16 :: A` resolves both to u16 — the literal's smallest-signed default no longer
|
||||||
|
blocks an unsigned demand; absent any demand it defaults to the smallest signed type
|
||||||
|
- a concrete declared type flows backward through a chain of bare-name references:
|
||||||
|
`X :: 1000; Y int :: X; Z i32 :: Y` resolves X and Y to i32 (previously they stayed
|
||||||
|
at the literal's i16)
|
||||||
|
- locals resolve identically to globals (no scope asymmetry): demands flow through
|
||||||
|
bare-name typed declarations, call arguments (a concrete parameter type demands its
|
||||||
|
argument, e.g. `take_u16(a)`), and returns — including from inside a function body
|
||||||
|
back onto a referenced global
|
||||||
|
- demands flow only through bare names; they do not cross arithmetic or other operators,
|
||||||
|
nor back across a call's result (the result-to-argument direction is milestone 14.5)
|
||||||
|
- a non-fitting or family-conflicting demand is not applied (first demand wins); the
|
||||||
|
genuine mismatch then surfaces as the usual boundary coercion error at the use
|
||||||
|
(e.g. `C u8 :: BIG` where `BIG :: 100000`)
|
||||||
|
|
||||||
15. add slice-by-range
|
14.5. backward type-demand propagation through call boundaries (deferred)
|
||||||
|
- a callee's result/return demand flows back through the function body to constrain
|
||||||
|
the caller's arguments, so `R u32 :: echo(A)` (with `echo :: func(p int) int`)
|
||||||
|
resolves A to u32 instead of erroring at the call's result coercion
|
||||||
|
- requires reversing the per-call data flow: a specialization's argument types
|
||||||
|
(`spec.args`) become outputs to solve, not just inputs — a new back-edge threaded
|
||||||
|
through every call site and the specialization fixpoint
|
||||||
|
- only meaningful on top of milestone 14's open constants
|
||||||
|
|
||||||
|
15. broaden type inference to infer type of declaration based on arithmetic expressions too
|
||||||
|
|
||||||
|
16. add slice-by-range
|
||||||
- allow the use of a range in slice expressions:
|
- allow the use of a range in slice expressions:
|
||||||
```
|
```
|
||||||
excl_range range :: 0..10
|
excl_range range :: 0..10
|
||||||
@@ -210,6 +241,22 @@
|
|||||||
some_arr[incl_range] # slice by named inclusive range
|
some_arr[incl_range] # slice by named inclusive range
|
||||||
```
|
```
|
||||||
|
|
||||||
|
17. for if statements, allow `if (cond) one-line statement` (instead of forcing either `if (cond) { block }` or `if cond { block }`)
|
||||||
|
- if statements without a bracketed body must enclose the condition in parentheses
|
||||||
|
|
||||||
|
18. add `defer` statement (inspired by zig)
|
||||||
|
|
||||||
|
19. multi-line strings (see below)
|
||||||
|
|
||||||
|
20. unions and tagged unions
|
||||||
|
|
||||||
|
21. match statements with tagged unions payload unwrapping
|
||||||
|
|
||||||
|
22. dynamic heap allocation
|
||||||
|
- see below for direction
|
||||||
|
- notes below are too big in scope for a first pass and the language is not mature enough to support it yet
|
||||||
|
- this first pass should focus on just basic heap allocation, so we have something to work with
|
||||||
|
|
||||||
## A word on multi-unwrap
|
## A word on multi-unwrap
|
||||||
|
|
||||||
Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated.
|
Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated.
|
||||||
@@ -276,7 +323,7 @@ This rule keeps the grammar simple and forces clarity at the call site — no pr
|
|||||||
|
|
||||||
For-loop captures are immutable and scoped to the loop body. Sequence index captures are `usize`. Pointer capture uses `|@item|`; arrays must be passed by pointer (for example `&items`), while slices can be used directly. Sentinel elements are not included in iteration.
|
For-loop captures are immutable and scoped to the loop body. Sequence index captures are `usize`. Pointer capture uses `|@item|`; arrays must be passed by pointer (for example `&items`), while slices can be used directly. Sentinel elements are not included in iteration.
|
||||||
|
|
||||||
# A word on distinct types
|
## A word on distinct types
|
||||||
|
|
||||||
Distinct types are considered distinct from their backing type. They do not implicitly coerce to their backing type.
|
Distinct types are considered distinct from their backing type. They do not implicitly coerce to their backing type.
|
||||||
|
|
||||||
@@ -288,7 +335,7 @@ UserID :: distinct u32
|
|||||||
my_id UserID :: UserID(42) # value must have the exact backing type
|
my_id UserID :: UserID(42) # value must have the exact backing type
|
||||||
```
|
```
|
||||||
|
|
||||||
# A word on enums
|
## A word on enums
|
||||||
|
|
||||||
```
|
```
|
||||||
# standard enums
|
# standard enums
|
||||||
@@ -347,3 +394,311 @@ value native.Imported_Enum :: native.IMPORTED_ENUM_VALUE
|
|||||||
|
|
||||||
The imported enum type is an alias of its target-selected C integer backing, and imported
|
The imported enum type is an alias of its target-selected C integer backing, and imported
|
||||||
enumerators are package-level constants.
|
enumerators are package-level constants.
|
||||||
|
|
||||||
|
## A word on multi-line strings
|
||||||
|
|
||||||
|
Multi-line strings use the `` ` `` character to mark each line. Content starts immediately after the backtick. Newlines between lines are implicit.
|
||||||
|
|
||||||
|
```
|
||||||
|
config =
|
||||||
|
`# Database configuration
|
||||||
|
`host = localhost
|
||||||
|
`port = 5432
|
||||||
|
`
|
||||||
|
`[server]
|
||||||
|
`address = 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Key properties:
|
||||||
|
|
||||||
|
* Content begins immediately after `` ` ``
|
||||||
|
* Newlines are automatically inserted between lines
|
||||||
|
* Empty `` ` `` produces a blank line
|
||||||
|
* No escape sequence processing (raw content)
|
||||||
|
* No trailing newline after the last line
|
||||||
|
|
||||||
|
Only the leading `` ` `` is special; the rest is treated as raw content.
|
||||||
|
|
||||||
|
If you need a trailing newline, add an empty line at the end:
|
||||||
|
|
||||||
|
```
|
||||||
|
# No trailing newline
|
||||||
|
msg =
|
||||||
|
`hello
|
||||||
|
`world
|
||||||
|
|
||||||
|
# With trailing newline
|
||||||
|
msg =
|
||||||
|
`hello
|
||||||
|
`world
|
||||||
|
`
|
||||||
|
```
|
||||||
|
|
||||||
|
Mixing multi-line strings with inline strings (using concatenation):
|
||||||
|
|
||||||
|
```
|
||||||
|
message =
|
||||||
|
"Header:\t" ++
|
||||||
|
`more content here
|
||||||
|
`even more content
|
||||||
|
`
|
||||||
|
++ "Footer"
|
||||||
|
```
|
||||||
|
|
||||||
|
Formatting alternative (purely aesthetics/preference, no effect on program):
|
||||||
|
|
||||||
|
```
|
||||||
|
message = "Header:\t" ++
|
||||||
|
`more content here
|
||||||
|
`even more content
|
||||||
|
`
|
||||||
|
++ "Footer"
|
||||||
|
```
|
||||||
|
|
||||||
|
## A word on memory allocation
|
||||||
|
|
||||||
|
(NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY)
|
||||||
|
(FURTHER, EXAMPLES ASSUME ARGUMENTS WITH DEFAULT VALUES AND COMPTIME POLYMORPHISM IN THE FORM OF GENERIC TYPE PARAMETERS - MONOMORPHISED)
|
||||||
|
|
||||||
|
Memory allocation in Brolang is designed to be **explicit but not verbose**. We reject the dogma that global state is inherently evil — allocators are a cross-cutting concern that nearly every function needs, making them a perfect candidate for sensible defaults.
|
||||||
|
|
||||||
|
### Philosophy
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ DESIGN PRINCIPLES │
|
||||||
|
│ │
|
||||||
|
│ 1. No hidden magic: allocation calls are visible │
|
||||||
|
│ 2. Sensible defaults: thread-local heap for common cases │
|
||||||
|
│ 3. Explicit override: custom allocators when needed │
|
||||||
|
│ 4. Build-mode aware: different behavior for debug/release │
|
||||||
|
│ 5. Immutable defaults: no "action at a distance" bugs │
|
||||||
|
│ 6. Escaping allocations: caller provides allocator │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Default Heap Allocator
|
||||||
|
|
||||||
|
Brolang provides a **thread-local global heap allocator** that is:
|
||||||
|
|
||||||
|
* Determined at compile time by build mode
|
||||||
|
* **Immutable at runtime** — cannot be reconfigured
|
||||||
|
|
||||||
|
```
|
||||||
|
import "std/mem/heap"
|
||||||
|
|
||||||
|
process :: func(input []u8) u64 {
|
||||||
|
# heap used for internal temporary work — does not escape
|
||||||
|
temp := heap.alloc(u8, size: input.len * 2)
|
||||||
|
defer heap.free(temp)
|
||||||
|
|
||||||
|
# ... work with temp ...
|
||||||
|
|
||||||
|
return compute_hash(temp) # only the result escapes, not the allocation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The behavior of `heap` depends on build mode:
|
||||||
|
|
||||||
|
| Build Mode | Allocator Behavior |
|
||||||
|
| -- | -- |
|
||||||
|
| Debug | Tracking allocator with leak detection |
|
||||||
|
| Release | Fast allocator, zero overhead |
|
||||||
|
| ReleaseSafe | Bounds-checking allocator |
|
||||||
|
|
||||||
|
This is configured at compile time. You cannot change which allocator `heap` uses at runtime. This is intentional — it prevents bugs where memory allocated with one allocator is freed with another.
|
||||||
|
|
||||||
|
### The Escaping Allocation Rule
|
||||||
|
|
||||||
|
**If a function heap-allocates memory that escapes its scope — whether via the return value or via writes through mutable parameters — the function must accept an allocator parameter.** The presence of an allocator parameter is the contract that says "heap memory escapes here, and you're responsible for it."
|
||||||
|
|
||||||
|
This rule makes ownership transfer visible at the function signature level. The caller never needs to read the function's implementation to know whether heap cleanup is involved:
|
||||||
|
|
||||||
|
```
|
||||||
|
import "std/mem"
|
||||||
|
import "std/mem/heap"
|
||||||
|
|
||||||
|
# Allocation escapes via return value — requires allocator
|
||||||
|
duplicate :: func(input []u8, allocator @mem.Allocator) []u8 {
|
||||||
|
result := allocator.alloc(u8, size: input.len)
|
||||||
|
mem.copy(result, input)
|
||||||
|
return result # caller manages this memory
|
||||||
|
}
|
||||||
|
|
||||||
|
# Allocation escapes via mutable parameter — requires allocator
|
||||||
|
init :: func(obj: @mut MyStruct, allocator: @mem.Allocator) void {
|
||||||
|
obj.buffer = allocator.alloc(u8, size: 100)
|
||||||
|
# caller now knows heap memory was written into obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# No allocation escapes — no allocator needed
|
||||||
|
process :: func(input: []u8) u64 {
|
||||||
|
temp := heap.alloc(u8, size: input.len)
|
||||||
|
defer heap.free(temp)
|
||||||
|
# ... work with temp ...
|
||||||
|
return compute_hash(temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
# No heap allocation at all — no allocator needed
|
||||||
|
reset :: func(obj: @mut MyStruct) void {
|
||||||
|
obj.count = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: func() void {
|
||||||
|
data := duplicate("hello", heap)
|
||||||
|
defer heap.free(data)
|
||||||
|
|
||||||
|
mut obj := MyStruct{ ... }
|
||||||
|
init(&obj, heap)
|
||||||
|
defer heap.free(obj.buffer)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this matters:**
|
||||||
|
|
||||||
|
* Without the rule, a function like `init(obj: @mut MyStruct) void` is ambiguous — did it heap-allocate into `obj`, or just set some fields to stack/static data? The caller has no way to know without reading the implementation.
|
||||||
|
* With the rule, the allocator parameter is a clear signal: "this function produces heap memory that outlives its scope, and you are responsible for cleaning it up."
|
||||||
|
* Internal allocations (temporary buffers, scratch space) use `heap` directly and are freed before the function returns. No allocator parameter needed, no burden on the caller.
|
||||||
|
|
||||||
|
**The compiler enforces this rule.** If a function heap-allocates memory that escapes without accepting an allocator parameter, the compiler emits an error.
|
||||||
|
|
||||||
|
### Why Immutable Defaults?
|
||||||
|
|
||||||
|
Consider what would happen if you could reconfigure the default allocator:
|
||||||
|
|
||||||
|
```
|
||||||
|
# ❌ THIS IS NOT ALLOWED (and doesn't exist in Brolang)
|
||||||
|
mem.heap_set(my_custom_heap)
|
||||||
|
|
||||||
|
# Somewhere else in the codebase...
|
||||||
|
data := heap.alloc(u8, size: 100)
|
||||||
|
|
||||||
|
# Later, someone changes it again...
|
||||||
|
mem.heap_set(different_heap)
|
||||||
|
|
||||||
|
# Now who frees `data`? With which allocator?
|
||||||
|
heap.free(data) # 💥 Wrong allocator - undefined behavior!
|
||||||
|
```
|
||||||
|
|
||||||
|
This is "action at a distance" — the behavior of `heap.free()` depends on what some unrelated code did earlier. By making `heap` immutable, Brolang guarantees:
|
||||||
|
|
||||||
|
**Whatever you allocate with, you free with.**
|
||||||
|
|
||||||
|
### Custom Allocators
|
||||||
|
|
||||||
|
For specialized needs, you create explicit allocator instances. These are not global — you manage their lifetime and pass them where needed.
|
||||||
|
|
||||||
|
**Arena Allocator**: Fast bump allocation, bulk deallocation:
|
||||||
|
|
||||||
|
```
|
||||||
|
import "std/mem"
|
||||||
|
import "std/mem/heap"
|
||||||
|
|
||||||
|
process_file :: func(path: []u8, allocator: @mem.Allocator) !Data {
|
||||||
|
# arena manages its own backing memory via heap
|
||||||
|
arena := mem.Arena.init(heap, capacity: mem.megabytes(1))
|
||||||
|
defer arena.deinit()
|
||||||
|
|
||||||
|
# all temporary allocations from arena (fast bump allocation)
|
||||||
|
file_contents := arena.alloc(u8, size: file_size)
|
||||||
|
parsed := arena.alloc(ParsedData) # size defaults to 1
|
||||||
|
tokens := arena.alloc(Token, size: 1000)
|
||||||
|
|
||||||
|
# ... process ...
|
||||||
|
|
||||||
|
# escaping allocation uses the caller's allocator
|
||||||
|
result := allocator.create(Data)
|
||||||
|
mem.copy(result, parsed)
|
||||||
|
|
||||||
|
return result
|
||||||
|
# arena.deinit() frees all arena memory — no individual frees needed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pool Allocator**: O(1) fixed-size allocation, no fragmentation:
|
||||||
|
|
||||||
|
```
|
||||||
|
import "std/mem"
|
||||||
|
import "std/mem/heap"
|
||||||
|
|
||||||
|
EntitySystem :: struct {
|
||||||
|
pool: mem.Pool(Entity),
|
||||||
|
}
|
||||||
|
|
||||||
|
init_entities :: func(allocator: @mem.Allocator) EntitySystem {
|
||||||
|
return EntitySystem{
|
||||||
|
pool = mem.Pool(Entity).init(allocator, capacity: 10_000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn :: func(sys: @mut EntitySystem) @Entity {
|
||||||
|
return sys.pool.alloc() # O(1), no fragmentation
|
||||||
|
}
|
||||||
|
|
||||||
|
despawn :: func(sys: @mut EntitySystem, entity: @Entity) void {
|
||||||
|
sys.pool.free(entity) # returned to pool for reuse
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Passing Allocators to Functions
|
||||||
|
|
||||||
|
As described in the escaping allocation rule, when a function heap-allocates memory that escapes its scope, it must accept an allocator parameter. The caller decides which allocator to use:
|
||||||
|
|
||||||
|
```
|
||||||
|
import "std/mem"
|
||||||
|
|
||||||
|
# Function that uses caller's allocator
|
||||||
|
parse :: func(input: []u8, allocator: @mem.Allocator) !ParseResult {
|
||||||
|
buffer := allocator.alloc(u8, size: input.len)
|
||||||
|
defer allocator.free(buffer)
|
||||||
|
|
||||||
|
# ... parse into buffer ...
|
||||||
|
|
||||||
|
result := allocator.alloc(ParseResult) # size defaults to 1
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
# Caller decides which allocator to use
|
||||||
|
main :: func() void {
|
||||||
|
# use an arena for this parsing work
|
||||||
|
arena := mem.Arena.init(heap, capacity: mem.kilobytes(64))
|
||||||
|
defer arena.deinit()
|
||||||
|
result := parse(input, &arena) catch |err| {
|
||||||
|
# handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
# or use a pool
|
||||||
|
pool := mem.Pool(ParseResult).init(capacity: 100)
|
||||||
|
defer pool.deinit()
|
||||||
|
result := parse(input, &pool) catch |err| {
|
||||||
|
# handle error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Allocation Summary
|
||||||
|
|
||||||
|
| What | How | When to Use |
|
||||||
|
| -- | -- | -- |
|
||||||
|
| `heap.alloc(T, size: n)` | Thread-local global | General purpose, 90% of cases |
|
||||||
|
| `heap.create(T)` | Thread-local global | Allocate single item |
|
||||||
|
| `allocator.alloc(T, size: n)` | Caller-provided | Escaping allocations (returned or written to caller's data) |
|
||||||
|
| `arena.alloc(T, size: n)` | Explicit instance | Temporary/scoped work, bulk free |
|
||||||
|
| `pool.alloc()` | Explicit instance | Many same-sized objects, O(1) |
|
||||||
|
|
||||||
|
Note that `heap` satisfies the `Allocator` interface, so callers can pass `heap` as the allocator argument when they don't need a specialized allocator — which is most of the time.
|
||||||
|
|
||||||
|
**The golden rule:** Allocate and free with the same allocator. The type system helps enforce this — memory from `heap` can only be freed with `heap`, memory from your arena can only be freed with that arena. The escaping allocation rule ensures the caller always knows which allocator was used.
|
||||||
|
|
||||||
|
### Compared to Other Languages
|
||||||
|
|
||||||
|
| Language | Approach | Brolang's Advantage |
|
||||||
|
| -- | -- | -- |
|
||||||
|
| C | Hidden malloc, easy to mismatch | Explicit allocator at call site |
|
||||||
|
| C++ | Allocator templates, complex | Simple, no template complexity |
|
||||||
|
| Rust | Explicit everywhere, verbose | Sensible defaults reduce noise |
|
||||||
|
| Zig | Allocator parameter threading | Only required for escaping allocations, not internal work |
|
||||||
|
| Odin | Hidden context parameter | Fully transparent, nothing hidden |
|
||||||
|
| Go | Hidden GC | Explicit control, no GC pauses |
|
||||||
|
|
||||||
|
Brolang sits in a sweet spot: explicit enough to always know what's happening, convenient enough that you don't drown in boilerplate.
|
||||||
|
|||||||
+294
-34
@@ -38,6 +38,12 @@ Infer_Local :: struct {
|
|||||||
declared: types.Type,
|
declared: types.Type,
|
||||||
statement: ast.Stmt_Id,
|
statement: ast.Stmt_Id,
|
||||||
mutable: bool,
|
mutable: bool,
|
||||||
|
// open_const marks a local whose initializer is a compile-time integer with no
|
||||||
|
// concrete annotation: like an open-constant global, it is sign-agnostic until a
|
||||||
|
// backward demand from a use picks its family/width (see merge_local_demand).
|
||||||
|
open_const: bool,
|
||||||
|
const_value: i128,
|
||||||
|
demanded: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
Build_Local :: struct {
|
Build_Local :: struct {
|
||||||
@@ -105,6 +111,14 @@ Checker :: struct {
|
|||||||
global_index: []Global_Index_Entry,
|
global_index: []Global_Index_Entry,
|
||||||
import_index: []Import_Index_Entry,
|
import_index: []Import_Index_Entry,
|
||||||
global_types: []types.Type,
|
global_types: []types.Type,
|
||||||
|
// Backward type-demand state for open-constant globals (milestone 14). global_demands
|
||||||
|
// accumulates demands reachable from any use (other globals' initializers and function
|
||||||
|
// bodies); global_demands_dirty lets a demand pushed from a function body re-trigger the
|
||||||
|
// inference fixpoint.
|
||||||
|
global_demands: []types.Type,
|
||||||
|
global_open_const: []bool,
|
||||||
|
global_const_value: []i128,
|
||||||
|
global_demands_dirty: bool,
|
||||||
external_global_canonical: []ast.Global_Id,
|
external_global_canonical: []ast.Global_Id,
|
||||||
external_global_diagnostics: []source.Diagnostic_Id,
|
external_global_diagnostics: []source.Diagnostic_Id,
|
||||||
constants: []Constant,
|
constants: []Constant,
|
||||||
@@ -1088,11 +1102,12 @@ infer_nested_expr :: proc(
|
|||||||
pkg: ast.Package_Id,
|
pkg: ast.Package_Id,
|
||||||
file: ast.File_Id,
|
file: ast.File_Id,
|
||||||
demanded: ^[dynamic]Spec_Id,
|
demanded: ^[dynamic]Spec_Id,
|
||||||
|
local_types: []types.Type = nil,
|
||||||
) -> types.Type {
|
) -> types.Type {
|
||||||
outer := checker.infer_stack
|
outer := checker.infer_stack
|
||||||
checker.infer_stack = nil
|
checker.infer_stack = nil
|
||||||
checker.infer_stack.allocator = checker.allocator
|
checker.infer_stack.allocator = checker.allocator
|
||||||
result := infer_expr(checker, expr_id, locals, pkg, file, demanded)
|
result := infer_expr(checker, expr_id, locals, pkg, file, demanded, local_types)
|
||||||
delete(checker.infer_stack)
|
delete(checker.infer_stack)
|
||||||
checker.infer_stack = outer
|
checker.infer_stack = outer
|
||||||
return result
|
return result
|
||||||
@@ -1105,21 +1120,22 @@ infer_compound_expr :: proc(
|
|||||||
pkg: ast.Package_Id,
|
pkg: ast.Package_Id,
|
||||||
file: ast.File_Id,
|
file: ast.File_Id,
|
||||||
demanded: ^[dynamic]Spec_Id,
|
demanded: ^[dynamic]Spec_Id,
|
||||||
|
local_types: []types.Type = nil,
|
||||||
) -> types.Type {
|
) -> types.Type {
|
||||||
store := &checker.module.types
|
store := &checker.module.types
|
||||||
#partial switch expr.kind {
|
#partial switch expr.kind {
|
||||||
case .Bool:
|
case .Bool:
|
||||||
return types.BOOL
|
return types.BOOL
|
||||||
case .Not:
|
case .Not:
|
||||||
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
return types.BOOL
|
return types.BOOL
|
||||||
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
|
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
|
||||||
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded)
|
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
|
||||||
return types.BOOL
|
return types.BOOL
|
||||||
case .Range:
|
case .Range:
|
||||||
left := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
left := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
right := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded)
|
right := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
|
||||||
left_const := eval_constant(checker, expr.left)
|
left_const := eval_constant(checker, expr.left)
|
||||||
right_const := eval_constant(checker, expr.right)
|
right_const := eval_constant(checker, expr.right)
|
||||||
child := types.INVALID
|
child := types.INVALID
|
||||||
@@ -1139,7 +1155,7 @@ infer_compound_expr :: proc(
|
|||||||
case .Array:
|
case .Array:
|
||||||
element := types.INVALID
|
element := types.INVALID
|
||||||
for arg in expr.args {
|
for arg in expr.args {
|
||||||
actual := infer_nested_expr(checker, arg, locals, pkg, file, demanded)
|
actual := infer_nested_expr(checker, arg, locals, pkg, file, demanded, local_types)
|
||||||
if !types.is_valid(element) {
|
if !types.is_valid(element) {
|
||||||
element = actual
|
element = actual
|
||||||
} else if !types.equal(element, actual) {
|
} else if !types.equal(element, actual) {
|
||||||
@@ -1157,25 +1173,25 @@ infer_compound_expr :: proc(
|
|||||||
case .Enum_Literal:
|
case .Enum_Literal:
|
||||||
return types.INVALID
|
return types.INVALID
|
||||||
case .Address:
|
case .Address:
|
||||||
child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
return types.pointer(store, child, false, false)
|
return types.pointer(store, child, false, false)
|
||||||
case .Deref:
|
case .Deref:
|
||||||
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
return types.child_type(value, store) if types.is_pointer(value, store) else types.INVALID
|
return types.child_type(value, store) if types.is_pointer(value, store) else types.INVALID
|
||||||
case .Index:
|
case .Index:
|
||||||
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded)
|
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
|
||||||
item, ok := types.container(value, store)
|
item, ok := types.container(value, store)
|
||||||
return item.child if ok else types.INVALID
|
return item.child if ok else types.INVALID
|
||||||
case .Slice:
|
case .Slice:
|
||||||
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
item, ok := types.container(value, store)
|
item, ok := types.container(value, store)
|
||||||
if !ok || item.kind == .Pointer {
|
if !ok || item.kind == .Pointer {
|
||||||
return types.INVALID
|
return types.INVALID
|
||||||
}
|
}
|
||||||
for bound in expr.args {
|
for bound in expr.args {
|
||||||
if bound != ast.INVALID_EXPR {
|
if bound != ast.INVALID_EXPR {
|
||||||
_ = infer_nested_expr(checker, bound, locals, pkg, file, demanded)
|
_ = infer_nested_expr(checker, bound, locals, pkg, file, demanded, local_types)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preserve := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR
|
preserve := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR
|
||||||
@@ -1185,7 +1201,7 @@ infer_compound_expr :: proc(
|
|||||||
_, member_ok := find_enum_member(checker, enum_type, expr.name)
|
_, member_ok := find_enum_member(checker, enum_type, expr.name)
|
||||||
return enum_type if member_ok else types.INVALID
|
return enum_type if member_ok else types.INVALID
|
||||||
}
|
}
|
||||||
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
field_name := symbol_text(checker, expr.name)
|
field_name := symbol_text(checker, expr.name)
|
||||||
item, has_item := types.container(value, store)
|
item, has_item := types.container(value, store)
|
||||||
if has_item && (item.kind == .Array || item.kind == .Slice) {
|
if has_item && (item.kind == .Array || item.kind == .Slice) {
|
||||||
@@ -1203,21 +1219,21 @@ infer_compound_expr :: proc(
|
|||||||
_, field, ok := find_struct_field(checker, value, expr.name)
|
_, field, ok := find_struct_field(checker, value, expr.name)
|
||||||
return field.type if ok else types.INVALID
|
return field.type if ok else types.INVALID
|
||||||
case .Unwrap:
|
case .Unwrap:
|
||||||
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID
|
return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID
|
||||||
case .Orelse:
|
case .Orelse:
|
||||||
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded)
|
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
|
||||||
return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID
|
return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID
|
||||||
case .Struct_Literal:
|
case .Struct_Literal:
|
||||||
for keyed in expr.args {
|
for keyed in expr.args {
|
||||||
_ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded)
|
_ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded, local_types)
|
||||||
}
|
}
|
||||||
target_pkg, available := expr_package(checker, expr, pkg, file)
|
target_pkg, available := expr_package(checker, expr, pkg, file)
|
||||||
value := types.find_named(store, u32(target_pkg), u32(expr.name)) if available else types.INVALID
|
value := types.find_named(store, u32(target_pkg), u32(expr.name)) if available else types.INVALID
|
||||||
return types.resolve_alias(value, store)
|
return types.resolve_alias(value, store)
|
||||||
case .Keyed:
|
case .Keyed:
|
||||||
return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
case:
|
case:
|
||||||
return types.INVALID
|
return types.INVALID
|
||||||
}
|
}
|
||||||
@@ -1230,6 +1246,7 @@ infer_expr :: proc(
|
|||||||
pkg := ast.Package_Id(0),
|
pkg := ast.Package_Id(0),
|
||||||
file := ast.File_Id(0),
|
file := ast.File_Id(0),
|
||||||
demanded: ^[dynamic]Spec_Id = nil,
|
demanded: ^[dynamic]Spec_Id = nil,
|
||||||
|
local_types: []types.Type = nil,
|
||||||
) -> types.Type {
|
) -> types.Type {
|
||||||
stack := checker.infer_stack
|
stack := checker.infer_stack
|
||||||
clear_dynamic_array(&stack)
|
clear_dynamic_array(&stack)
|
||||||
@@ -1281,7 +1298,7 @@ infer_expr :: proc(
|
|||||||
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
|
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
|
||||||
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Enum_Literal,
|
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Enum_Literal,
|
||||||
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
|
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
|
||||||
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded)
|
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types)
|
||||||
_ = pop(&stack)
|
_ = pop(&stack)
|
||||||
case .Name:
|
case .Name:
|
||||||
last = types.INVALID
|
last = types.INVALID
|
||||||
@@ -1349,7 +1366,7 @@ infer_expr :: proc(
|
|||||||
append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION})
|
append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION})
|
||||||
case .Call:
|
case .Call:
|
||||||
if expr.left != ast.INVALID_EXPR {
|
if expr.left != ast.INVALID_EXPR {
|
||||||
callee_type := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
|
callee_type := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||||
_, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types)
|
_, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types)
|
||||||
if !ok {
|
if !ok {
|
||||||
last = types.INVALID
|
last = types.INVALID
|
||||||
@@ -1464,6 +1481,13 @@ infer_expr :: proc(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function := checker.ast_module.functions[frame.template]
|
function := checker.ast_module.functions[frame.template]
|
||||||
|
// A bare-name argument passed to a concrete (non-constraint) parameter pushes
|
||||||
|
// that parameter type back onto the argument's slot, so an open constant adopts
|
||||||
|
// it (e.g. `take_u16(a)` resolves `a` to u16). Constraint params have no single
|
||||||
|
// type to demand; the callee's result flowing back is milestone 14.5.
|
||||||
|
for arg_index in 0..<len(expr.args) {
|
||||||
|
record_demand(checker, expr.args[arg_index], call_arg_expected(function, arg_index), locals, local_types, pkg, file)
|
||||||
|
}
|
||||||
if valid_call_arity(function, len(expr.args)) &&
|
if valid_call_arity(function, len(expr.args)) &&
|
||||||
can_specialize(checker, function, stack[frame_index].args) {
|
can_specialize(checker, function, stack[frame_index].args) {
|
||||||
spec := INVALID_SPEC
|
spec := INVALID_SPEC
|
||||||
@@ -1612,7 +1636,7 @@ infer_statements :: proc(
|
|||||||
declared_local := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
|
declared_local := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
|
||||||
value_type := types.INVALID
|
value_type := types.INVALID
|
||||||
if !is_undefined_expr(checker, statement.expr) {
|
if !is_undefined_expr(checker, statement.expr) {
|
||||||
value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
|
value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||||
}
|
}
|
||||||
if is_runtime_type(checker, declared_local) {
|
if is_runtime_type(checker, declared_local) {
|
||||||
value_type = declared_local
|
value_type = declared_local
|
||||||
@@ -1621,19 +1645,33 @@ infer_statements :: proc(
|
|||||||
// build_block reports). FLOAT defaults integers to f64.
|
// build_block reports). FLOAT defaults integers to f64.
|
||||||
value_type = types.constraint_target(declared_local, value_type, &checker.module.types)
|
value_type = types.constraint_target(declared_local, value_type, &checker.module.types)
|
||||||
}
|
}
|
||||||
|
open := false
|
||||||
|
const_val := i128(0)
|
||||||
|
if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) {
|
||||||
|
constant := eval_constant(checker, statement.expr)
|
||||||
|
if constant.kind == .Value && fits_i64(constant.value) {
|
||||||
|
open = true
|
||||||
|
const_val = constant.value
|
||||||
|
}
|
||||||
|
}
|
||||||
local := Infer_Local{
|
local := Infer_Local{
|
||||||
name=statement.name,
|
name=statement.name,
|
||||||
type=value_type,
|
type=value_type,
|
||||||
declared=declared_local,
|
declared=declared_local,
|
||||||
statement=statement_id,
|
statement=statement_id,
|
||||||
mutable=!statement.immutable,
|
mutable=!statement.immutable,
|
||||||
|
open_const=open,
|
||||||
|
const_value=const_val,
|
||||||
}
|
}
|
||||||
append(locals, local)
|
append(locals, local)
|
||||||
record_infer_local_type(local, local_types)
|
record_infer_local_type(local, local_types)
|
||||||
|
// A typed/constraint declaration initialized by a bare name pushes its resolved
|
||||||
|
// type backward onto that name (mirrors the `Y int :: X; Z i32 :: Y` global chain).
|
||||||
|
record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file)
|
||||||
case .Assignment:
|
case .Assignment:
|
||||||
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
|
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||||
if statement.target != ast.INVALID_EXPR {
|
if statement.target != ast.INVALID_EXPR {
|
||||||
_ = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded)
|
_ = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types)
|
||||||
target_expr := checker.ast_module.exprs[statement.target]
|
target_expr := checker.ast_module.exprs[statement.target]
|
||||||
if target_expr.kind == .Name && !symbol.is_valid(target_expr.qualifier) {
|
if target_expr.kind == .Name && !symbol.is_valid(target_expr.qualifier) {
|
||||||
if local_index, ok := find_infer_local_index(locals^[:], target_expr.name); ok &&
|
if local_index, ok := find_infer_local_index(locals^[:], target_expr.name); ok &&
|
||||||
@@ -1648,16 +1686,23 @@ infer_statements :: proc(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .Expression:
|
case .Expression:
|
||||||
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
|
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||||
case .Return:
|
case .Return:
|
||||||
if statement.expr != ast.INVALID_EXPR {
|
if statement.expr != ast.INVALID_EXPR {
|
||||||
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
|
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||||
if is_runtime_type(checker, result_hint) {
|
if is_runtime_type(checker, result_hint) {
|
||||||
expr := checker.ast_module.exprs[statement.expr]
|
expr := checker.ast_module.exprs[statement.expr]
|
||||||
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
|
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
|
||||||
if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok {
|
if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok {
|
||||||
_ = merge_infer_local_type(checker, &locals^[local_index], result_hint, local_types)
|
// Open constants can adopt the result type cross-family; other
|
||||||
|
// locals widen within family as before.
|
||||||
|
if !merge_local_demand(checker, &locals^[local_index], result_hint, local_types) {
|
||||||
|
_ = merge_infer_local_type(checker, &locals^[local_index], result_hint, local_types)
|
||||||
|
}
|
||||||
returned = result_hint
|
returned = result_hint
|
||||||
|
} else {
|
||||||
|
// `return G` for a global const: demand the result type onto it.
|
||||||
|
record_demand(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1674,7 +1719,7 @@ infer_statements :: proc(
|
|||||||
flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &operands)
|
flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &operands)
|
||||||
operand_types := make([]types.Type, len(operands), checker.allocator)
|
operand_types := make([]types.Type, len(operands), checker.allocator)
|
||||||
for operand, index in operands {
|
for operand, index in operands {
|
||||||
operand_types[index] = infer_expr(checker, operand, locals^[:], pkg, file, demanded)
|
operand_types[index] = infer_expr(checker, operand, locals^[:], pkg, file, demanded, local_types)
|
||||||
}
|
}
|
||||||
capture_start := len(locals^)
|
capture_start := len(locals^)
|
||||||
for capture, index in statement.captures {
|
for capture, index in statement.captures {
|
||||||
@@ -1689,7 +1734,7 @@ infer_statements :: proc(
|
|||||||
append(locals, Infer_Local{name=capture, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT})
|
append(locals, Infer_Local{name=capture, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT})
|
||||||
}
|
}
|
||||||
if statement.guard != ast.INVALID_EXPR {
|
if statement.guard != ast.INVALID_EXPR {
|
||||||
_ = infer_expr(checker, statement.guard, locals^[:], pkg, file, demanded)
|
_ = infer_expr(checker, statement.guard, locals^[:], pkg, file, demanded, local_types)
|
||||||
}
|
}
|
||||||
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
||||||
resize(locals, capture_start)
|
resize(locals, capture_start)
|
||||||
@@ -1697,19 +1742,19 @@ infer_statements :: proc(
|
|||||||
delete(operand_types, checker.allocator)
|
delete(operand_types, checker.allocator)
|
||||||
delete(operands)
|
delete(operands)
|
||||||
} else {
|
} else {
|
||||||
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
|
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||||
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
||||||
infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint)
|
infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint)
|
||||||
}
|
}
|
||||||
case .While:
|
case .While:
|
||||||
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
|
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||||
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
|
||||||
if statement.update != ast.INVALID_STMT {
|
if statement.update != ast.INVALID_STMT {
|
||||||
update := [1]ast.Stmt_Id{statement.update}
|
update := [1]ast.Stmt_Id{statement.update}
|
||||||
infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint)
|
infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint)
|
||||||
}
|
}
|
||||||
case .For:
|
case .For:
|
||||||
iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
|
iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||||
capture_start := len(locals^)
|
capture_start := len(locals^)
|
||||||
capture_type := types.INVALID
|
capture_type := types.INVALID
|
||||||
if types.is_range(iterable_type, &checker.module.types) {
|
if types.is_range(iterable_type, &checker.module.types) {
|
||||||
@@ -1797,11 +1842,158 @@ merge_inferred_type :: proc(store: ^types.Store, current: ^types.Type, inferred:
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// root_demand_target returns the global that an initializer pushes a backward type
|
||||||
|
// demand onto: when the initializer's root expression is a bare name referencing a
|
||||||
|
// global (e.g. `Z i32 :: Y`). Returns INVALID_GLOBAL for any other shape — demands
|
||||||
|
// deliberately do not flow through arithmetic, calls, or other operators (that is L3).
|
||||||
|
root_demand_target :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> ast.Global_Id {
|
||||||
|
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
||||||
|
return ast.INVALID_GLOBAL
|
||||||
|
}
|
||||||
|
expr := checker.ast_module.exprs[expr_id]
|
||||||
|
if expr.kind != .Name {
|
||||||
|
return ast.INVALID_GLOBAL
|
||||||
|
}
|
||||||
|
target_pkg, available := expr_package(checker, expr, pkg, file)
|
||||||
|
if !available {
|
||||||
|
return ast.INVALID_GLOBAL
|
||||||
|
}
|
||||||
|
return find_global(checker, expr.name, target_pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge_open_const_demand records a concrete integer demand onto an open-constant
|
||||||
|
// global's slot. The constant is sign-agnostic until used, so it may adopt any
|
||||||
|
// integer family/width whose range holds its value (first demand wins; later demands
|
||||||
|
// may only widen within the chosen family). Non-integer or non-fitting demands are
|
||||||
|
// ignored, leaving the constant to default and the genuine mismatch to surface at the
|
||||||
|
// use's boundary coercion.
|
||||||
|
merge_open_const_demand :: proc(checker: ^Checker, slot: ^types.Type, demand: types.Type, value: i128) -> bool {
|
||||||
|
if !types.is_concrete_integer(demand) || !fits_integer_type(value, demand, checker.target) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !is_runtime_type(checker, slot^) {
|
||||||
|
slot^ = demand
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if types.equal(slot^, demand) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
merged := types.widest(slot^, demand)
|
||||||
|
if types.is_concrete_scalar(merged) && !types.equal(slot^, merged) {
|
||||||
|
slot^ = merged
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge_global_demand routes a concrete demand onto a global's slot: open constants
|
||||||
|
// adopt any fitting family, other referents widen within family. Sets a dirty flag so
|
||||||
|
// a demand pushed from a function body re-triggers the inference fixpoint.
|
||||||
|
merge_global_demand :: proc(checker: ^Checker, global: ast.Global_Id, demand: types.Type) -> bool {
|
||||||
|
index := int(global)
|
||||||
|
if index < 0 || index >= len(checker.global_demands) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
changed: bool
|
||||||
|
if checker.global_open_const[index] {
|
||||||
|
changed = merge_open_const_demand(checker, &checker.global_demands[index], demand, checker.global_const_value[index])
|
||||||
|
} else {
|
||||||
|
changed = merge_inferred_type(&checker.module.types, &checker.global_demands[index], demand)
|
||||||
|
}
|
||||||
|
checker.global_demands_dirty = checker.global_demands_dirty || changed
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge_local_demand records a concrete integer demand onto an open-constant local.
|
||||||
|
// Like an open-constant global it adopts any integer family/width whose range holds its
|
||||||
|
// value (gated by its constraint family if it has one); the first demand replaces the
|
||||||
|
// literal's signed default, later demands may only widen within the chosen family.
|
||||||
|
merge_local_demand :: proc(checker: ^Checker, local: ^Infer_Local, demand: types.Type, local_types: []types.Type) -> bool {
|
||||||
|
if !local.open_const || !types.is_concrete_integer(demand) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if types.is_constraint(local.declared) &&
|
||||||
|
!types.constraint_accepts(local.declared, demand, &checker.module.types) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !fits_integer_type(local.const_value, demand, checker.target) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !local.demanded {
|
||||||
|
local.type = demand
|
||||||
|
local.demanded = true
|
||||||
|
record_infer_local_type(local^, local_types)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if types.equal(local.type, demand) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
merged := types.widest(local.type, demand)
|
||||||
|
if types.is_concrete_scalar(merged) && !types.equal(local.type, merged) {
|
||||||
|
local.type = merged
|
||||||
|
record_infer_local_type(local^, local_types)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// record_demand pushes a concrete type demand onto the slot of a bare-name expression
|
||||||
|
// (a typed declaration's initializer, a call argument, a return value). When the name
|
||||||
|
// resolves to an open-constant local or global, that slot adopts the demand; any other
|
||||||
|
// shape is ignored — demands flow only through bare names, never through arithmetic or
|
||||||
|
// across a call's result (the latter is milestone 14.5).
|
||||||
|
record_demand :: proc(
|
||||||
|
checker: ^Checker,
|
||||||
|
expr_id: ast.Expr_Id,
|
||||||
|
demand: types.Type,
|
||||||
|
locals: []Infer_Local,
|
||||||
|
local_types: []types.Type,
|
||||||
|
pkg: ast.Package_Id,
|
||||||
|
file: ast.File_Id,
|
||||||
|
) {
|
||||||
|
if !is_runtime_type(checker, demand) ||
|
||||||
|
expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expr := checker.ast_module.exprs[expr_id]
|
||||||
|
if expr.kind != .Name {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !symbol.is_valid(expr.qualifier) {
|
||||||
|
if index, ok := find_infer_local_index(locals, expr.name); ok {
|
||||||
|
_ = merge_local_demand(checker, &locals[index], demand, local_types)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target_pkg, available := expr_package(checker, expr, pkg, file)
|
||||||
|
if !available {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
global := find_global(checker, expr.name, target_pkg)
|
||||||
|
if global != ast.INVALID_GLOBAL {
|
||||||
|
_ = merge_global_demand(checker, global, demand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
infer_all :: proc(checker: ^Checker) {
|
infer_all :: proc(checker: ^Checker) {
|
||||||
|
// An "open constant" global has no concrete declared type and a compile-time
|
||||||
|
// integer initializer. Its slot stays sign-agnostic so a backward demand from any
|
||||||
|
// reachable use can pick its family/width; absent a demand it defaults to the
|
||||||
|
// smallest signed type (legacy behaviour). Demands accumulate in global_demands so
|
||||||
|
// the default never blocks a later cross-family (e.g. unsigned) demand.
|
||||||
for global, index in checker.ast_module.globals {
|
for global, index in checker.ast_module.globals {
|
||||||
declared := type_from_syntax(global.type)
|
declared := type_from_syntax(global.type)
|
||||||
if is_runtime_type(checker, declared) {
|
if is_runtime_type(checker, declared) {
|
||||||
checker.global_types[index] = declared
|
checker.global_types[index] = declared
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if global.external {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
constant := eval_constant(checker, global.expr)
|
||||||
|
if constant.kind == .Value && fits_i64(constant.value) {
|
||||||
|
checker.global_open_const[index] = true
|
||||||
|
checker.global_const_value[index] = constant.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1812,7 +2004,29 @@ infer_all :: proc(checker: ^Checker) {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
changed := false
|
changed := false
|
||||||
|
checker.global_demands_dirty = false
|
||||||
spec_count := len(checker.specs)
|
spec_count := len(checker.specs)
|
||||||
|
|
||||||
|
// Backward demands: a global whose initializer's root is a bare name referencing
|
||||||
|
// another global pushes its own (declared or already-resolved) type onto that
|
||||||
|
// referent. Open constants adopt any fitting family; other referents widen only.
|
||||||
|
for global, index in checker.ast_module.globals {
|
||||||
|
if global.external {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
demand := checker.global_types[index]
|
||||||
|
if !is_runtime_type(checker, demand) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
target := root_demand_target(checker, global.expr, global.pkg, global.file)
|
||||||
|
if target != ast.INVALID_GLOBAL {
|
||||||
|
merge_global_demand(checker, target, demand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward / resolution. infer_expr runs for every non-external global (even
|
||||||
|
// concrete-typed ones) for its side effect of specializing called functions and
|
||||||
|
// recording demands from call arguments in their initializers.
|
||||||
for global, index in checker.ast_module.globals {
|
for global, index in checker.ast_module.globals {
|
||||||
if global.external {
|
if global.external {
|
||||||
continue
|
continue
|
||||||
@@ -1821,8 +2035,24 @@ infer_all :: proc(checker: ^Checker) {
|
|||||||
if is_runtime_type(checker, type_from_syntax(global.type)) {
|
if is_runtime_type(checker, type_from_syntax(global.type)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed
|
if is_runtime_type(checker, checker.global_demands[index]) {
|
||||||
|
// A backward demand is authoritative; assign directly (it may cross the
|
||||||
|
// signed/unsigned family that widening would reject).
|
||||||
|
if !types.equal(checker.global_types[index], checker.global_demands[index]) {
|
||||||
|
checker.global_types[index] = checker.global_demands[index]
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
} else if checker.global_open_const[index] {
|
||||||
|
resolved := types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
|
||||||
|
if !types.equal(checker.global_types[index], resolved) {
|
||||||
|
checker.global_types[index] = resolved
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for index := 0; index < len(checker.specs); index += 1 {
|
for index := 0; index < len(checker.specs); index += 1 {
|
||||||
id := spec_id(index)
|
id := spec_id(index)
|
||||||
inferred := infer_spec_result(checker, id)
|
inferred := infer_spec_result(checker, id)
|
||||||
@@ -1831,6 +2061,11 @@ infer_all :: proc(checker: ^Checker) {
|
|||||||
if len(checker.specs) != spec_count {
|
if len(checker.specs) != spec_count {
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
// A demand pushed onto a global from inside a function body (via the spec loop)
|
||||||
|
// is picked up by the next pass's resolution, so keep iterating for it.
|
||||||
|
if checker.global_demands_dirty {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
if !changed {
|
if !changed {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -3487,9 +3722,19 @@ build_block :: proc(
|
|||||||
switch statement.kind {
|
switch statement.kind {
|
||||||
case .Declaration:
|
case .Declaration:
|
||||||
declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
|
declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr)
|
||||||
|
// Adopt the type inference resolved for this local when the declaration has no
|
||||||
|
// concrete annotation (a constraint, `undefined`, or an un-annotated open
|
||||||
|
// integer constant): the slot may have absorbed a backward demand (e.g. `a :: 10`
|
||||||
|
// built as u16 after `take_u16(a)`). Gated to compile-time integer constants so
|
||||||
|
// strings/arrays/pointers keep their own initializer type.
|
||||||
|
open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr)
|
||||||
|
if open_const_decl {
|
||||||
|
constant := eval_constant(checker, statement.expr)
|
||||||
|
open_const_decl = constant.kind == .Value && fits_i64(constant.value)
|
||||||
|
}
|
||||||
if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) &&
|
if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) &&
|
||||||
is_runtime_type(checker, ctx.local_types[statement_id]) &&
|
is_runtime_type(checker, ctx.local_types[statement_id]) &&
|
||||||
(types.is_constraint(declared) || is_undefined_expr(checker, statement.expr)) {
|
(types.is_constraint(declared) || is_undefined_expr(checker, statement.expr) || open_const_decl) {
|
||||||
declared = ctx.local_types[statement_id]
|
declared = ctx.local_types[statement_id]
|
||||||
}
|
}
|
||||||
// A still-unresolved constraint means the initializer's numeric
|
// A still-unresolved constraint means the initializer's numeric
|
||||||
@@ -4352,6 +4597,15 @@ build_globals :: proc(checker: ^Checker) {
|
|||||||
expected := types.INVALID
|
expected := types.INVALID
|
||||||
if is_runtime_type(checker, declared) {
|
if is_runtime_type(checker, declared) {
|
||||||
expected = declared
|
expected = declared
|
||||||
|
} else if constant := eval_constant(checker, global.expr);
|
||||||
|
constant.kind == .Value && fits_i64(constant.value) &&
|
||||||
|
is_runtime_type(checker, checker.global_types[global_index]) {
|
||||||
|
// Open constant: build the initializer against the type inference resolved
|
||||||
|
// for this slot, so it adopts its demanded/defaulted type (e.g. `A :: 10`
|
||||||
|
// built as u16 when a use demanded u16). Gated to compile-time values fitting
|
||||||
|
// i64 — exactly the infer-side open-constant condition — so out-of-range
|
||||||
|
// constants keep their original "exceeds signed i64 range" diagnostic.
|
||||||
|
expected = checker.global_types[global_index]
|
||||||
}
|
}
|
||||||
expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected, global.pkg, global.file)
|
expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected, global.pkg, global.file)
|
||||||
global_type := checker.global_types[global_index]
|
global_type := checker.global_types[global_index]
|
||||||
@@ -4645,6 +4899,9 @@ check :: proc(
|
|||||||
checker.cycle_stack.allocator = allocator
|
checker.cycle_stack.allocator = allocator
|
||||||
build_symbol_indexes(&checker)
|
build_symbol_indexes(&checker)
|
||||||
checker.global_types = make([]types.Type, len(ast_module.globals), allocator)
|
checker.global_types = make([]types.Type, len(ast_module.globals), allocator)
|
||||||
|
checker.global_demands = make([]types.Type, len(ast_module.globals), allocator)
|
||||||
|
checker.global_open_const = make([]bool, len(ast_module.globals), allocator)
|
||||||
|
checker.global_const_value = make([]i128, len(ast_module.globals), allocator)
|
||||||
checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator)
|
checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator)
|
||||||
checker.external_global_diagnostics = make([]source.Diagnostic_Id, len(ast_module.globals), allocator)
|
checker.external_global_diagnostics = make([]source.Diagnostic_Id, len(ast_module.globals), allocator)
|
||||||
for &canonical in checker.external_global_canonical {
|
for &canonical in checker.external_global_canonical {
|
||||||
@@ -4667,6 +4924,9 @@ check :: proc(
|
|||||||
delete(checker.global_index, allocator)
|
delete(checker.global_index, allocator)
|
||||||
delete(checker.import_index, allocator)
|
delete(checker.import_index, allocator)
|
||||||
delete(checker.global_types, allocator)
|
delete(checker.global_types, allocator)
|
||||||
|
delete(checker.global_demands, allocator)
|
||||||
|
delete(checker.global_open_const, allocator)
|
||||||
|
delete(checker.global_const_value, allocator)
|
||||||
delete(checker.external_global_canonical, allocator)
|
delete(checker.external_global_canonical, allocator)
|
||||||
delete(checker.external_global_diagnostics, allocator)
|
delete(checker.external_global_diagnostics, allocator)
|
||||||
delete(checker.constants, allocator)
|
delete(checker.constants, allocator)
|
||||||
|
|||||||
@@ -6427,3 +6427,205 @@ main :: func() void {
|
|||||||
}
|
}
|
||||||
testing.expect_value(t, len(diagnostics.items), 0)
|
testing.expect_value(t, len(diagnostics.items), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
contextual_inference_resolves_signed_const_chain :: proc(t: ^testing.T) {
|
||||||
|
text := `X :: 1000
|
||||||
|
Y int :: X
|
||||||
|
Z i32 :: Y
|
||||||
|
main :: func() void {}
|
||||||
|
`
|
||||||
|
source_file := source.Source{path="test.bro", text=text}
|
||||||
|
diagnostics := source.init_diagnostics(&source_file)
|
||||||
|
defer source.destroy_diagnostics(&diagnostics)
|
||||||
|
symbols := symbol.init_table()
|
||||||
|
defer symbol.destroy_table(&symbols)
|
||||||
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
||||||
|
defer delete(stream.items)
|
||||||
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
||||||
|
defer ast.destroy_module(&ast_module)
|
||||||
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
||||||
|
defer hir.destroy_module(&hir_module)
|
||||||
|
|
||||||
|
// The concrete i32 on Z flows backward through Y to the open constant X, so all
|
||||||
|
// three resolve to i32 instead of X/Y staying at the literal's smallest signed type.
|
||||||
|
testing.expect_value(t, len(diagnostics.items), 0)
|
||||||
|
testing.expect(t, types.equal(hir_module.globals[0].type, types.I32))
|
||||||
|
testing.expect(t, types.equal(hir_module.globals[1].type, types.I32))
|
||||||
|
testing.expect(t, types.equal(hir_module.globals[2].type, types.I32))
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
contextual_inference_open_constants_adopt_unsigned_demand :: proc(t: ^testing.T) {
|
||||||
|
text := `A :: 10
|
||||||
|
B u16 :: A
|
||||||
|
P :: 10
|
||||||
|
R u32 :: P
|
||||||
|
N :: 42
|
||||||
|
main :: func() void {}
|
||||||
|
`
|
||||||
|
source_file := source.Source{path="test.bro", text=text}
|
||||||
|
diagnostics := source.init_diagnostics(&source_file)
|
||||||
|
defer source.destroy_diagnostics(&diagnostics)
|
||||||
|
symbols := symbol.init_table()
|
||||||
|
defer symbol.destroy_table(&symbols)
|
||||||
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
||||||
|
defer delete(stream.items)
|
||||||
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
||||||
|
defer ast.destroy_module(&ast_module)
|
||||||
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
||||||
|
defer hir.destroy_module(&hir_module)
|
||||||
|
|
||||||
|
// An open constant is sign-agnostic until used: it adopts the unsigned family a use
|
||||||
|
// demands (the literal's signed default would block this). Unconstrained N defaults.
|
||||||
|
testing.expect_value(t, len(diagnostics.items), 0)
|
||||||
|
testing.expect(t, types.equal(hir_module.globals[0].type, types.U16)) // A
|
||||||
|
testing.expect(t, types.equal(hir_module.globals[1].type, types.U16)) // B
|
||||||
|
testing.expect(t, types.equal(hir_module.globals[2].type, types.U32)) // P
|
||||||
|
testing.expect(t, types.equal(hir_module.globals[3].type, types.U32)) // R
|
||||||
|
testing.expect(t, types.equal(hir_module.globals[4].type, types.I8)) // N
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
contextual_inference_rejects_constant_that_does_not_fit_demand :: proc(t: ^testing.T) {
|
||||||
|
text := `BIG :: 100000
|
||||||
|
C u8 :: BIG
|
||||||
|
main :: func() void {}
|
||||||
|
`
|
||||||
|
source_file := source.Source{path="test.bro", text=text}
|
||||||
|
diagnostics := source.init_diagnostics(&source_file)
|
||||||
|
defer source.destroy_diagnostics(&diagnostics)
|
||||||
|
symbols := symbol.init_table()
|
||||||
|
defer symbol.destroy_table(&symbols)
|
||||||
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
||||||
|
defer delete(stream.items)
|
||||||
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
||||||
|
defer ast.destroy_module(&ast_module)
|
||||||
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
||||||
|
defer hir.destroy_module(&hir_module)
|
||||||
|
|
||||||
|
// 100000 does not fit u8, so the demand is rejected, BIG defaults to i32, and the
|
||||||
|
// genuine mismatch surfaces at the use's boundary coercion.
|
||||||
|
found := false
|
||||||
|
for diagnostic in diagnostics.items {
|
||||||
|
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
|
||||||
|
}
|
||||||
|
testing.expect(t, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
contextual_inference_does_not_cross_call_boundaries :: proc(t: ^testing.T) {
|
||||||
|
text := `echo :: func(p int) int { return p }
|
||||||
|
A :: 10
|
||||||
|
R u32 :: echo(A)
|
||||||
|
main :: func() void {}
|
||||||
|
`
|
||||||
|
source_file := source.Source{path="test.bro", text=text}
|
||||||
|
diagnostics := source.init_diagnostics(&source_file)
|
||||||
|
defer source.destroy_diagnostics(&diagnostics)
|
||||||
|
symbols := symbol.init_table()
|
||||||
|
defer symbol.destroy_table(&symbols)
|
||||||
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
||||||
|
defer delete(stream.items)
|
||||||
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
||||||
|
defer ast.destroy_module(&ast_module)
|
||||||
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
||||||
|
defer hir.destroy_module(&hir_module)
|
||||||
|
|
||||||
|
// The u32 demand on R must not flow through echo into A (that is L3, deferred). A
|
||||||
|
// stays at its default i8, so the call result fails to coerce to u32.
|
||||||
|
found := false
|
||||||
|
for diagnostic in diagnostics.items {
|
||||||
|
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i8 to u32")
|
||||||
|
}
|
||||||
|
testing.expect(t, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
contextual_inference_resolves_locals_like_globals :: proc(t: ^testing.T) {
|
||||||
|
text := `take_u16 :: func(v u16) void {}
|
||||||
|
get :: func() u16 {
|
||||||
|
c :: 10
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
main :: func() void {
|
||||||
|
x :: 1000
|
||||||
|
y int :: x
|
||||||
|
z i32 :: y
|
||||||
|
a :: 10
|
||||||
|
b u16 :: a
|
||||||
|
n :: 5
|
||||||
|
take_u16(n)
|
||||||
|
_ = get()
|
||||||
|
}
|
||||||
|
`
|
||||||
|
source_file := source.Source{path="test.bro", text=text}
|
||||||
|
diagnostics := source.init_diagnostics(&source_file)
|
||||||
|
defer source.destroy_diagnostics(&diagnostics)
|
||||||
|
symbols := symbol.init_table()
|
||||||
|
defer symbol.destroy_table(&symbols)
|
||||||
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
||||||
|
defer delete(stream.items)
|
||||||
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
||||||
|
defer ast.destroy_module(&ast_module)
|
||||||
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
||||||
|
defer hir.destroy_module(&hir_module)
|
||||||
|
|
||||||
|
// The same backward propagation works for locals: a constant local adopts the
|
||||||
|
// unsigned/wider type a later use demands (declaration, call argument, or return),
|
||||||
|
// so none of these need an explicit annotation. Without it, i8->u16/u32 would error.
|
||||||
|
testing.expect_value(t, len(diagnostics.items), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
contextual_inference_demand_from_function_body_reaches_global :: proc(t: ^testing.T) {
|
||||||
|
text := `take_u16 :: func(v u16) void {}
|
||||||
|
G :: 10
|
||||||
|
main :: func() void {
|
||||||
|
take_u16(G)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
source_file := source.Source{path="test.bro", text=text}
|
||||||
|
diagnostics := source.init_diagnostics(&source_file)
|
||||||
|
defer source.destroy_diagnostics(&diagnostics)
|
||||||
|
symbols := symbol.init_table()
|
||||||
|
defer symbol.destroy_table(&symbols)
|
||||||
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
||||||
|
defer delete(stream.items)
|
||||||
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
||||||
|
defer ast.destroy_module(&ast_module)
|
||||||
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
||||||
|
defer hir.destroy_module(&hir_module)
|
||||||
|
|
||||||
|
// A demand originating inside a function body (passing G to a u16 parameter) flows
|
||||||
|
// back to the open-constant global G, resolving it to u16.
|
||||||
|
testing.expect_value(t, len(diagnostics.items), 0)
|
||||||
|
testing.expect(t, types.equal(hir_module.globals[0].type, types.U16))
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testing.T) {
|
||||||
|
text := `main :: func() void {
|
||||||
|
big :: 100000
|
||||||
|
c u8 :: big
|
||||||
|
}
|
||||||
|
`
|
||||||
|
source_file := source.Source{path="test.bro", text=text}
|
||||||
|
diagnostics := source.init_diagnostics(&source_file)
|
||||||
|
defer source.destroy_diagnostics(&diagnostics)
|
||||||
|
symbols := symbol.init_table()
|
||||||
|
defer symbol.destroy_table(&symbols)
|
||||||
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
||||||
|
defer delete(stream.items)
|
||||||
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
||||||
|
defer ast.destroy_module(&ast_module)
|
||||||
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
||||||
|
defer hir.destroy_module(&hir_module)
|
||||||
|
|
||||||
|
// 100000 does not fit u8, so big keeps its i32 default and the use errors.
|
||||||
|
found := false
|
||||||
|
for diagnostic in diagnostics.items {
|
||||||
|
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
|
||||||
|
}
|
||||||
|
testing.expect(t, found)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user