test(ir): lock pure Obj-C decision helpers before A6.1 extraction (A6.1 scaffolding step 1)

Test-first scaffolding for the Obj-C FFI domain (Phase A6.1) before the pure
helpers move into src/ir/ffi_objc.zig. Visibility-only change to the targets —
no behavior change.

- 3 new lower.test.zig tests for the pure helpers the ARCH-SAFETY A6.1 row names
  that lacked direct unit coverage:
  - deriveObjcSelector: niladic (bare name) / single-keyword (name:) /
    multi-keyword (_ -> : + trailing) / #selector(...) override (verbatim,
    keyword_count = #colons).
  - objcPropertyKind: assign default (primitive), strong default (object ptr),
    explicit weak/copy/assign win over the default.
  - isObjcClassPointer: pointer-to-foreign-Obj-C-class true; plain-struct ptr /
    *void / builtin false.
- objcTypeEncodingFromSignature (x6) + objcDefinedStateStructType (x3) already
  covered — no new tests.
- Widened deriveObjcSelector + objcPropertyKind to pub (they become facade
  methods in step 2; the ObjcPropertyKind enum stays private — tests compare via
  enum-literal == .strong). No logic touched.
- Recorded the A6.1 coverage inventory + residual gaps (resolveObjcParentName,
  class-method metadata, property/state lookups — example-guarded) in
  ARCH-SAFETY.md.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(no .ir churn; Obj-C snapshots 1309/1329/1332/1347 green).
This commit is contained in:
agra
2026-06-03 07:15:56 +03:00
parent d346bbb677
commit 0012228796
2 changed files with 117 additions and 2 deletions

View File

@@ -616,6 +616,121 @@ test "lower: objcTypeEncodingFromSignature emits nested structs (CGRect)" {
try std.testing.expectEqualStrings("v@:{CGRect={CGPoint=dd}{CGSize=dd}}", e2);
}
// ── A6.1 scaffolding: pure Obj-C decision helpers ───────────────────
// Lock selector derivation, property-kind classification, and Obj-C
// class-pointer recognition before they move to `ffi_objc.zig`.
fn objcMethod(name: []const u8) ast.ForeignMethodDecl {
return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null };
}
test "lower: deriveObjcSelector — niladic / keyword / multi-keyword / override" {
const alloc = std.testing.allocator;
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var lowering = Lowering.init(&module);
// arity 0 → bare name, no colons, not an override.
const niladic = lowering.deriveObjcSelector(objcMethod("count"), 0);
try std.testing.expectEqualStrings("count", niladic.sel);
try std.testing.expectEqual(@as(usize, 0), niladic.keyword_count);
try std.testing.expectEqual(false, niladic.is_override);
// arity ≥ 1, no `_` → single trailing colon, one keyword.
const single = lowering.deriveObjcSelector(objcMethod("setValue"), 1);
defer alloc.free(single.sel);
try std.testing.expectEqualStrings("setValue:", single.sel);
try std.testing.expectEqual(@as(usize, 1), single.keyword_count);
try std.testing.expectEqual(false, single.is_override);
// each `_` → `:`, plus a trailing `:`; piece count = (#`_`) + 1.
const multi = lowering.deriveObjcSelector(objcMethod("setValue_forKey"), 2);
defer alloc.free(multi.sel);
try std.testing.expectEqualStrings("setValue:forKey:", multi.sel);
try std.testing.expectEqual(@as(usize, 2), multi.keyword_count);
try std.testing.expectEqual(false, multi.is_override);
// `#selector(...)` override: used verbatim, keyword_count = #colons.
var m = objcMethod("init_with_frame_style");
m.selector_override = "initWithFrame:style:";
const overridden = lowering.deriveObjcSelector(m, 2);
try std.testing.expectEqualStrings("initWithFrame:style:", overridden.sel);
try std.testing.expectEqual(@as(usize, 2), overridden.keyword_count);
try std.testing.expectEqual(true, overridden.is_override);
}
test "lower: isObjcClassPointer recognises pointer-to-foreign-Obj-C-class" {
const alloc = std.testing.allocator;
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var lowering = Lowering.init(&module);
// *NSString where NSString is a registered Obj-C class → true.
const ns_name = module.types.internString("NSString");
const ns_struct = module.types.intern(.{ .@"struct" = .{ .name = ns_name, .fields = &.{} } });
const ns_ptr = module.types.ptrTo(ns_struct);
var ns_fcd = ast.ForeignClassDecl{
.name = "NSString",
.foreign_path = "NSString",
.runtime = .objc_class,
.members = &.{},
.is_foreign = true,
.is_main = false,
};
try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd);
try std.testing.expect(lowering.isObjcClassPointer(ns_ptr));
// *Plain where Plain is a non-foreign struct → false.
const plain_name = module.types.internString("Plain");
const plain_struct = module.types.intern(.{ .@"struct" = .{ .name = plain_name, .fields = &.{} } });
try std.testing.expect(!lowering.isObjcClassPointer(module.types.ptrTo(plain_struct)));
// *void and a builtin scalar → false (not object pointers).
try std.testing.expect(!lowering.isObjcClassPointer(module.types.ptrTo(.void)));
try std.testing.expect(!lowering.isObjcClassPointer(.s32));
}
test "lower: objcPropertyKind defaults + explicit ARC modifiers" {
const alloc = std.testing.allocator;
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var lowering = Lowering.init(&module);
// Register NSString so `*NSString` resolves to an object pointer.
const ns_name = module.types.internString("NSString");
_ = module.types.intern(.{ .@"struct" = .{ .name = ns_name, .fields = &.{} } });
var ns_fcd = ast.ForeignClassDecl{
.name = "NSString",
.foreign_path = "NSString",
.runtime = .objc_class,
.members = &.{},
.is_foreign = true,
.is_main = false,
};
try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd);
// Primitive field, no modifiers → assign (the non-object default).
const prim = ast.ForeignFieldDecl{ .name = "count", .field_type = typeKeyword(alloc, "s32"), .is_property = true };
defer alloc.destroy(prim.field_type);
try std.testing.expect(lowering.objcPropertyKind(prim) == .assign);
// Object-pointer field, no modifiers → strong (the object default).
const obj_ty = typeKeyword(alloc, "*NSString");
defer alloc.destroy(obj_ty);
const obj_default = ast.ForeignFieldDecl{ .name = "title", .field_type = obj_ty, .is_property = true };
try std.testing.expect(lowering.objcPropertyKind(obj_default) == .strong);
// Explicit modifiers on an object pointer win over the default.
const weak_mods = [_][]const u8{"weak"};
try std.testing.expect(lowering.objcPropertyKind(.{ .name = "delegate", .field_type = obj_ty, .is_property = true, .property_modifiers = &weak_mods }) == .weak);
const copy_mods = [_][]const u8{"copy"};
try std.testing.expect(lowering.objcPropertyKind(.{ .name = "name", .field_type = obj_ty, .is_property = true, .property_modifiers = &copy_mods }) == .copy);
const assign_mods = [_][]const u8{"assign"};
try std.testing.expect(lowering.objcPropertyKind(.{ .name = "raw", .field_type = obj_ty, .is_property = true, .property_modifiers = &assign_mods }) == .assign);
}
// ── Pack projection name resolution (Feature 1, Step 2.2) ────────────
const errors = @import("../errors.zig");

View File

@@ -6031,7 +6031,7 @@ pub const Lowering = struct {
/// - arity ≥ 1: split the sx name on `_`; each piece becomes a
/// keyword with a trailing `:` (`addObject` → `addObject:`,
/// `combine_and` → `combine:and:`).
fn deriveObjcSelector(self: *Lowering, method: ast.ForeignMethodDecl, arity: usize) struct { sel: []const u8, keyword_count: usize, is_override: bool } {
pub fn deriveObjcSelector(self: *Lowering, method: ast.ForeignMethodDecl, arity: usize) struct { sel: []const u8, keyword_count: usize, is_override: bool } {
if (method.selector_override) |sel| {
var colons: usize = 0;
for (sel) |ch| {
@@ -15790,7 +15790,7 @@ pub const Lowering = struct {
/// - `weak` on a non-object field type → diagnostic
/// - `strong` (explicit or defaulted) on `*void` (ambiguous: Obj-C
/// object vs raw memory) → require explicit modifier
fn objcPropertyKind(self: *Lowering, field: ast.ForeignFieldDecl) ObjcPropertyKind {
pub fn objcPropertyKind(self: *Lowering, field: ast.ForeignFieldDecl) ObjcPropertyKind {
// Survey the modifier list.
var has_strong = false;
var has_weak = false;