Merge branch 'flow/distribution/fix-0101'
Some checks failed
Build / build-linux (push) Has been cancelled
Build / build-windows (push) Has been cancelled

This commit is contained in:
agra
2026-06-06 07:47:23 +03:00
6 changed files with 152 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
// Postfix `!` (optional force-unwrap) chained directly with a member access.
// `opt!.field`, `opt!.method()`, `opt!.a.b`, and `opt![i]` must read the same
// value the bind-first form (`v := opt!; v.field`) produces — the unwrapped
// value's type has to flow into the chained access.
//
// Regression (issue 0101): chained `opt!.field` typed its receiver as
// `.unresolved` (inferExprType had no force_unwrap arm), so a string field read
// as garbage and `opt!.method()` failed to resolve at all.
#import "modules/std.sx";
Inner :: struct { tag: string; k: s64; }
S :: struct {
id: string;
n: s64;
inner: Inner;
greet :: (self: *S) -> string { return self.id; } // pointer receiver
bump :: (self: S, extra: s64) -> s64 { return self.n + extra; } // value receiver
}
mk :: () -> ?S {
return S.{ id = "hello", n = 42, inner = Inner.{ tag = "deep", k = 7 } };
}
arr :: () -> ?[3]s64 {
v : [3]s64 = .[10, 20, 30];
return v;
}
main :: () -> void {
// opt!.field — string and int field, chained vs bind-first.
print("chain id: {}\n", mk()!.id); // hello
print("chain n: {}\n", mk()!.n); // 42
v := mk()!;
print("bind id: {}\n", v.id); // hello
print("bind n: {}\n", v.n); // 42
// opt!.method()
print("meth ptr: {}\n", mk()!.greet()); // hello
print("meth val: {}\n", mk()!.bump(8)); // 50
// nested opt!.a.b
print("nest tag: {}\n", mk()!.inner.tag); // deep
print("nest k: {}\n", mk()!.inner.k); // 7
// opt![i]
print("index 0: {}\n", arr()![0]); // 10
print("index 2: {}\n", arr()![2]); // 30
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,10 @@
chain id: hello
chain n: 42
bind id: hello
bind n: 42
meth ptr: hello
meth val: 50
nest tag: deep
nest k: 7
index 0: 10
index 2: 30

View File

@@ -0,0 +1,76 @@
# 0101 — postfix `!` chained with `.field` miscompiles
**RESOLVED.** A postfix optional force-unwrap chained directly with a member
access — `opt!.field` — read garbage instead of the field, while the
bind-first form (`v := opt!; v.field`) was correct. Sibling chains shared the
bug: `opt!.method()` failed to resolve the method at all (`error: unresolved
'<method>'`), and `opt!.a.b` / `opt![i]` were affected the same way.
**Symptom** — observed vs expected:
```sx
#import "modules/std.sx";
S :: struct { id: string; n: s64; }
mk :: () -> ?S { return S.{ id = "hello", n = 42 }; }
main :: () {
print("chained: {}\n", mk()!.id); // observed: garbage (e.g. 8362783136)
v := mk()!; print("bind: {}\n", v.id); // observed: "hello" (correct)
}
```
Expected: `mk()!.id` prints `hello` (same as the bind-first form).
**Root cause** (`src/ir/expr_typer.zig`, `ExprTyper.inferType`): the AST-level
type-inference switch had **no `.force_unwrap` arm**, so `mk()!` typed as
`.unresolved` (the `else` fallback). `lowerForceUnwrap` lowers the unwrap to a
correctly-typed value, so the *bind* form works — `v := mk()!` stores that
typed Ref into a slot and `v.field` reads it back. But the *chained* form never
materializes a slot: `lowerFieldAccess` re-derives the receiver type via
`inferExprType(fa.object)` (= `inferExprType(mk()!)`), got `.unresolved`, and
the struct-field lookup on `.unresolved` failed — `mk()!.id` was typed
`.unresolved`/`s64` and its value emitted as `undef` (the print monomorphized
`pack_s64` with `i64 undef`, surfacing as a stale stack address). The method
chain failed for the same reason: receiver typing returned `.unresolved`, so
method resolution found nothing.
**Fix** (`src/ir/expr_typer.zig:92`): add a `.force_unwrap` arm to
`ExprTyper.inferType` that resolves the operand's optional child type (mirrors
`lowerForceUnwrap`'s `resolveOptionalInner`):
```zig
.force_unwrap => |fu| blk: {
const opt_ty = self.l.inferExprType(fu.operand);
if (!opt_ty.isBuiltin()) {
const info = self.l.module.types.get(opt_ty);
if (info == .optional) break :blk info.optional.child;
}
break :blk .unresolved;
},
```
This is the single root cause for every chained form — field, nested field,
method call, and index all route their receiver type through `inferExprType`.
One arm fixes all of them.
**Reproduction** (standalone, std-only):
```sx
#import "modules/std.sx";
Inner :: struct { tag: string; k: s64; }
S :: struct {
id: string; n: s64; inner: Inner;
greet :: (self: *S) -> string { return self.id; }
}
mk :: () -> ?S { return S.{ id = "hello", n = 42, inner = Inner.{ tag = "deep", k = 7 } }; }
main :: () {
print("{}\n", mk()!.id); // pre-fix: garbage; post-fix: hello
print("{}\n", mk()!.n); // pre-fix: garbage; post-fix: 42
print("{}\n", mk()!.greet()); // pre-fix: error unresolved 'greet'; post-fix: hello
print("{}\n", mk()!.inner.tag); // post-fix: deep
}
```
**Regression**: `examples/0905-optionals-unwrap-field-chain.sx` exercises
`opt!.field` (string + int field, chained vs bind-first), `opt!.method()`
(pointer + value receiver), nested `opt!.a.b`, and `opt![i]`. Garbage / compile
error on pre-fix code; all correct after.

View File

@@ -84,6 +84,19 @@ pub const ExprTyper = struct {
if (op_ty == channel) break :blk .void;
break :blk self.l.failableSuccessType(op_ty);
},
// `opt!` force-unwraps an optional to its child type. Without this
// arm a chained `opt!.field` / `opt![i]` / `opt!.method()` would
// type its receiver as `.unresolved` (the `else` below) and fail to
// resolve — even though `lowerForceUnwrap` produces a correctly
// typed value. Mirrors lowerForceUnwrap's resolveOptionalInner.
.force_unwrap => |fu| blk: {
const opt_ty = self.l.inferExprType(fu.operand);
if (!opt_ty.isBuiltin()) {
const info = self.l.module.types.get(opt_ty);
if (info == .optional) break :blk info.optional.child;
}
break :blk .unresolved;
},
.caller_location => self.l.module.types.findByName(self.l.module.types.internString("Source_Location")) orelse .unresolved,
.if_expr => |ie| {
// If-else types as its branches' unified type. A `noreturn`