چندریختی

    ۱. اصول بنیادین و قراردادهای واژگانی

    کوروش از تبدیل ضمنی نوع (implicit type coercion) و جریمه‌های پنهان کارایی جلوگیری می‌کند. چندریختی (Polymorphism) به‌طور دقیق بین پارامترهای ایستا و انتزاع‌های پویای صریح تقسیم می‌شود و از مرزهای مشخص حوزه ماژول پیروی می‌کند.

    ۱.۱ واژگان و طبقه‌بندی

    • نوع انضمامی (Concrete Type): یک struct یا enum کاملاً وضوح‌یافته که در فضای نام یک ماژول تعریف شده است.
    • پارامتر نوع جنریک (Generic Type Parameter):) یک مکان‌نشان زمان کامپایل (T) که با یک محدودیت رابط (interface constraint) مقید شده است.
    • رابط (Interface): یک قرارداد نام‌دار که مجموعه‌ای از امضای توابع را تعریف می‌کند.
    • **شیء رابط (Interface Object):) یک نوع داده ساختاری زمان اجرا (اشاره‌گر چاق یا fat pointer) که به‌طور صریح توسط عملگر dynamic تولید می‌شود.

    ۱.۲ نحو محدود به ماژول

    رابط‌ها، structها و enumها از قوانین دید استاندارد ماژول در کوروش پیروی می‌کنند. نمادها به طور پیش‌فرض خصوصی هستند مگر اینکه با pub علامت‌گذاری شوند.

    // shape_types.cyrus
    import std::libc{printf};
    
    pub interface Shape<T> {
        fn area(&const self) T;
    }
    
    // Structها با استفاده از دو نقطه `:` پایبندی به رابط را اعلام می‌کنند
    pub struct Square : Shape<float64> {
        side: float64;
    
        pub fn new(const side: float64) Self {
            return Self { side };
        }
    
        pub fn area(&const self) float64 {
            return self->side * self->side;
        }
    }
    
    

    ۲. معماری دوگانه توزیع

    کوروش دو استراتژی کاملاً مجزا برای بازاستفاده کد و چندریختی پیاده‌سازی می‌کند. توسعه‌دهنده با استفاده از نحوی صریح در مرز اعلان، کامپایلر را هدایت می‌کند.

    ۲.۱ تک‌شکلی در محل فراخوانی (جنریک‌های ایستا)

    هنگامی که یک تابع چندریختی را از طریق محدودیت‌های نوع (مانند <T: Interface>) متصل می‌کند، کامپایلر از تک‌شکلی در محل فراخوانی (Call-Site Monomorphization) استفاده می‌کند.

    // توزیع ایستا: کاملاً در زمان کامپایل وضوح می‌یابد
    pub fn compute_area<T: Shape<float64>>(shape: T) -> float64 {
        return shape.area();
    }
    
    
    • استراتژی کامپایل: کامپایلر درخت نحو انتزاعی (AST) تابع compute_area را برای هر نوع انضمامی منحصربه‌فردی که در سراسر گراف ماژول به آن ارسال می‌شود، تکثیر می‌کند.
    • کارایی: فراخوانی‌های تابع مستقیم و بدون واسطه. امکان بهینه‌سازی‌های کامل (درون‌خطی‌سازی، باز کردن حلقه، حذف کد مرده) را فراهم می‌کند.
    • معایب: در صورت اعمال بی‌رویه در سراسر پایگاه‌های کد بزرگ، می‌تواند منجر به حجیم شدن باینری شود.

    ۲.۲ تک‌شکلی در محل ایجاد (رابط‌های پویا)

    هنگامی که یک تابع نام یک رابط را مستقیماً به عنوان نوع پارامتر بدون براکت‌های جنریک می‌پذیرد، یک شیء رابط (Interface Object) را انتظار دارد.

    // توزیع پویا: در زمان اجرا از طریق یک اشاره‌گر چاق وضوح می‌یابد
    pub fn print_area(shape: Shape<float64>) {
        // پرش غیرمستقیم از طریق جدول مجازی محلی شیء
        printf("%f\n", shape.area());
    }
    
    
    • استراتژی کامپایل: کامپایلر با Shape<float64> به عنوان یک نوع ساختاری انضمامی متشکل از یک اشاره‌گر داده و یک اشاره‌گر جدول مجازی صریح رفتار می‌کند.
    • کارایی: جریمه یک واسطه‌گری واحد. با تولید دقیقاً یک امضای پاک‌شده از نوع، از حجیم شدن باینری جلوگیری می‌کند.

    ۳. چیدمان زمان اجرا و عملگر dynamic

    اشیاء رابط هرگز به‌طور ضمنی ایجاد نمی‌شوند. برای تبدیل یک struct یا enum انضمامی به یک انتزاع منطبق با رابط، توسعه‌دهندگان باید از عبارت صریح dynamic استفاده کنند.

    ۳.۱ بازنمایی دودویی

    یک شیء رابط مستقیماً به عنوان یک جفت ساختاری با اندازه ثابت (اشاره‌گر چاق) پیاده‌سازی می‌شود:

    struct InterfaceObject {
        const data_ptr: void*;      // اشاره‌گر خام به حافظه نمونه (هپ/پشته)
        const vtable_ptr: void*;    // اشاره‌گر به vtable تولیدشده در محل ایجاد
    }
    

    ۳.۲ چرخه عمر تولید در محل ایجاد

    هنگامی که کامپایلر عبارتی مانند const obj: Shape<float64> = dynamic Square.new(10.3); را تجزیه می‌کند، یک چرخه عمر کامپایل محلی را فعال می‌کند:

    1. تأیید: کامپایلر تأیید می‌کند که Square به‌طور کامل الزامات Shape<float64> را برآورده می‌کند.
    2. تک‌شکلی محلی: کامپایلر اطمینان حاصل می‌کند که آدرس‌های کد برای تمام متدهای مورد نیاز Square که به Shape<float64> مقید شده‌اند، وجود دارند.
    3. تخصیص Vtable: یک جدول مجازی ایستا و ایمن از نظر نخ که حاوی اشاره‌گرهای تابع خاص است، در بخش داده ماژول در حال کامپایل جاسازی می‌شود.
    4. ساخت اشاره‌گر چاق: یک مقدار پشته‌ای ۲-کلمه‌ای مونتاژ می‌شود که آدرس نمونه انضمامی را با آدرس منحصربه‌فرد vtable محلی بسته‌بندی می‌کند.
    شیء رابط (اشاره‌گر چاق)
    ┌───────────────────────────┐
    │ data_ptr                  ├──────► [ داده‌های نمونه Square (side: 10.3) ]
    ├───────────────────────────┤
    │ vtable_ptr                ├──────► [ Vtable تک‌شکلی‌شده ]
    └───────────────────────────┘
    

    ۴. بهینه‌سازی ایستا مبتنی بر اجبار

    در حالی که توزیع زمان اجرا انعطاف‌پذیری نوع را تضمین می‌کند، تحلیلگر معنایی کوروش به‌طور مداوم نگاشت‌های نوع انضمامی را در حوزه‌های واژگانی محلی ردیابی می‌کند. اگر یک InterfaceObject در یک حوزه قابل ردیابی مورد پرسش قرار گیرد، فراخوانی مجازی اجباراً (coerced) به یک فراخوانی ایستای مستقیم بازگردانده می‌شود.

    ۴.۱ سناریوهای بهینه‌سازی

    import std::libc{printf};
    import shape_types{Shape, Square};
    
    pub fn main() {
        // بافت ۱: مقداردهی متغیر محلی
        const shape: Shape<float64> = dynamic Square.new(10.3);
    
        // بهینه‌سازی شده: تحلیلگر معنایی می‌داند 'shape' به‌طور قطعی یک 'Square' است
        printf("%f\n", shape.area());
    
        // بافت ۲: مرزهای حوزه (از دست دادن دانش نوع محلی)
        const shapes = Shape<float64>[2] {
            dynamic Square.new(10.3),
            shape
        };
    
        // بهینه‌سازی نشده: تغییرپذیری اندیس آرایه ارزیابی ایستا را غیرممکن می‌کند
        // کامپایلر خروجی می‌دهد: فراخوانی غیرمستقیم از طریق vtable_ptr
        for (var i = 0; i < 2; i++) {
            printf("%f\n", shapes[i].area());
        }
    }
    

    ۴.۲ موتور قوانین بهینه‌سازی


    عبارت بافتمسیر وضوحاقدام بهینه‌سازی

    local_variable.method()

    ایستا

    جستجوی vtable را کاملاً دور می‌زند. به یک فراخوانی مستقیم بی‌واسطه تبدیل می‌شود.

    inline_function(interface_obj)

    ایستا

    حوزه تابع را درون‌خطی می‌کند و فراداده بهینه‌سازی نوع محلی را حفظ می‌نماید.

    standard_function(interface_obj)

    پویا

    اطلاعات نوع در مرز پارامتر رها می‌شود؛ به اندیس‌های vtable زمان اجرا متکی است.

    array_index[i].method()

    پویا

    اندیس‌های ساختاری زمان اجرا منطق جستجوی خطی را دور می‌زنند. همیشه پویا.

    ۵. مثال‌های کد و رفتار کامپایلر

    مثال‌های زیر نحوه پردازش چندریختی توسط کامپایلر کوروش را در سناریوهای مختلف، از جنریک‌های ایستا تا اشیاء رابط زمان اجرا، نشان می‌دهند.

    ۵.۱ Enumها و دو مسیر توزیع

    این مثال نشان می‌دهد که چگونه انواع struct و enum هر دو می‌توانند رابط‌ها را پیاده‌سازی کنند، و برجسته می‌کند که کامپایلر بر اساس نحوه ارسال داده، یا یک فراخوانی ایستای مستقیم یا یک جستجوی vtable پویا را انتخاب می‌کند.

    import std::libc{printf};
    
    interface MyConstraint {
        fn foo(&self, x: int) int;
        fn bar(&self) char*;
    }
    
    struct Object : MyConstraint {
        name: char*;
    
        pub fn new(name: char*) Self {
            return Self { name };
        }
        pub fn foo(&self, x: int) int {
            return (x * 10) + 2;
        }
        pub fn bar(&self) char* {
            return self->name;
        }
    }
    
    enum Choice : MyConstraint {
        A,
        B,
    
        pub fn foo(&self, x: int) int {
            return -x;
        }
        pub fn bar(&self) char* {
            return "Choice";
        }
    }
    
    // تک‌شکلی در محل فراخوانی: کاملاً به پرش‌های ایستای تخصصی‌شده پاک‌سازی می‌شود
    fn constraint_bar<T: MyConstraint>(object: T) {
        printf("%s %d | ", object.bar(), object.foo(3));
    }
    
    // توزیع پویای محل ایجاد: یک چیدمان اشاره‌گر چاق یکنواخت دریافت می‌کند
    fn dynamic_interface(object: MyConstraint) {
        printf("%s %d | ", object.bar(), object.foo(5));
    }
    
    pub fn main() {
        const object1 = Object.new("Cyrus");
        const object2 = Choice.B;
    
        // ۱. چندریختی ایستا
        constraint_bar(object1);
        constraint_bar(object2);
    
        // ۲. چندریختی پویا
        const dynamic1: MyConstraint = dynamic object1;
        dynamic_interface(dynamic1);
        dynamic_interface(dynamic object1);
    }
    

    ۵.۲ نمونه‌سازی رابط جدا شده با محدودیت‌های جنریک

    نمایش یک مؤلفه اعتبارسنج جنریک که انواع اولیه انضمامی صریح را در یک مرز پویا مدیریت می‌کند.

    import std::libc{printf};
    
    interface IValidator<T> {
        fn validate(&const self, value: T) bool;
    }
    
    struct NumberValidator: IValidator<int> {
    	pub fn validate(&const self, value: int) bool {
    		if (value > 5) {
    			return true;
    		} else {
    			return false;
    		}
    	}
    }
    
    pub fn main() {
    	const validator: IValidator<int> = dynamic NumberValidator{};
    
    	if (validator.validate(10)) {
    		printf("yes\n");
    	} else {
    		printf("no\n");
    	}
    }
    

    ۵.۳ قابلیت تعامل سطح پایین (تخصیص‌دهنده‌های چیدمان حافظه سیستم)

    نشان می‌دهد که رابط‌های کوروش به‌طور ایمن در پایین‌ترین سطح برنامه‌نویسی سیستمی فیزیکی عمل می‌کنند و منابع هپ را مستقیماً از طریق اشاره‌گرهای سیستمی دستی بدون یک محفظه زمان اجرا مدیریت می‌نمایند.

    import std::libc{printf, malloc, free};
    
    interface Allocator {
        fn alloc(&self, size: usize) void*;
        fn free(&self, ptr: void*) void;
    }
    
    struct HeapAllocator : Allocator {
        pub fn new() Self {
            return Self {};
        }
    
        pub fn alloc(&const self, size: usize) void* {
            return malloc(size);
        }
    
        pub fn free(&const self, ptr: void*) void {
            free(ptr);
        }
    }
    
    pub fn main() {
        const allocator = HeapAllocator.new();
    
        var ptr: int* = allocator.alloc(4);
    
        *ptr = 5;
        printf("%d\n", *ptr);
    
        allocator.free(ptr);
    }