SmartKitio

البرمجة

ماذا يحدث فعلياً حين تعمل الشفرة؟

البرنامج على شاشتك نص. والبرنامج الذي يعمل ذاكرة وسجلات واستدعاءات نظام (syscalls) تتغيّر مع الزمن. بين العالمين خط أنابيب — تحليل lexical، وتحليل نحوي، وتحليل دلالي، وتوليد شفرة، وتحسين، وخدمات وقت التشغيل — يفسّر أخطاء الصياغة وتتبّعات المكدس والفجوة بين «يعمل على جهازي» والإنتاج.

7 دقائق قراءة · آخر تحديث 2026-10-04

مساحة عمل مطوّر مع شفرة على الشاشة وإضاءة مكتب دافئة
صورة عبر Unsplash · رخصة Unsplash

الشفرة المصدرية ليست برنامجاً بعد

ملفات الشفرة المصدرية أحرف مُرمّزة بـ UTF-8 (أو ترميز آخر تتوقعه سلسلة أدواتك). نظام التشغيل لا ينفّذ النص مباشرة — بل يحمّل صيغة تنفيذية (ELF أو Mach-O أو PE) أو يشغّل وقت تشغيل يفسّر البايتكود. المهمة الأولى لتطبيق لغة هي تحويل النص إلى تمثيل منظّم يمكنه الاستدلال عليه.

من النص إلى البنية

خط أنابيب كلاسيكي للترجمة / التنفيذ
  1. Source

    ملفات .rs و.java و.js و.py يكتبها البشر

  2. Lexer

    مسح الأحرف إلى tokens (كلمات مفتاحية، معرّفات، قيم حرفية)

  3. Parser

    بناء شجرة بناء مجردة (AST) وفق قواعد النحو

  4. Semantic analysis

    الأنواع والنطاقات والمتغيرات غير المعرّفة — أخطاء قبل التشغيل

  5. IR / bytecode / machine code

    خفض AST إلى عمليات محمولة أو تعليمات أصلية

  6. Optimizer

    Inlining، إزالة الشفرة الميتة، إعادة ترتيب للمعالج (مرور اختياري)

  7. Runtime / CPU

    تنفيذ على العتاد أو آلة افتراضية مع خدمات نظام التشغيل

المراحل الدقيقة تختلف حسب اللغة؛ اتجاه تدفّق البيانات واحد.

أخطاء الصياغة تفشل في المحلّل lexical أو المحلّل النحوي («unexpected token»). أخطاء النوع أو الاسم تفشل غالباً في التحليل الدلالي. أخطاء المنطق تجتاز كل الفحوص وتظهر فقط حين تصل مدخلات محددة إلى فروع معيبة — لذلك الاختبارات والقابلية للمراقبة مهمان بعد نجاح خط الأنابيب.

التفسير مقابل الترجمة

التفسير المحض يجوب AST أو البايتكود وينفّذ الإجراءات فوراً — بسيط للبدء، أبطأ لكل خطوة. الترجمة ahead-of-time (AOT) تترجم برامج كاملة إلى شفرة آلة قبل التنفيذ — إصدارات C وRust وGo تتصرّف هكذا. كثير من اللغات الشائعة تمزج الاثنين: Python يترجم إلى بايتكود ثم يفسّر؛ Java تشحن بايتكود وJIT يترجم الدوال الساخنة؛ JavaScript يُحلّل مرة واحدة، وينفّذ مسارات سريعة عبر طبقات JIT، ويعود إلى تفسير أبطأ حين تتغيّر الأنواع.

استراتيجيات التنفيذ (مبسّطة)

الاستراتيجيةالفكرةأمثلة
التفسير بجولة على الشجرةزيارة عقد AST والتصرّفمفسّرات تعليمية مبكرة
Bytecode + VMالترجمة إلى تعليمات محمولة؛ الـ VM يوزّع opcodesبايتكود CPython، JVM لـ Java، IL لـ C#
ترجمة أصلية AOTشفرة آلة قبل التشغيلRust، C release، Go افتراضياً
JITترجمة المسارات الساخنة وقت التشغيلمحرّكات JavaScript (V8، SpiderMonkey)، JVM HotSpot
Transpile ثم تشغيلمصدر إلى مصدر، ثم وقت تشغيل موجودTypeScript → JavaScript

المفسّر مقابل AOT مقابل JIT (متى يفوز كل منها)

الأسلوبمتى يُنجَز العملالبدءالسرعة القصوىالمقايضة النموذجية
مفسّر / VM بايتكودكل تعليمة وقت التشغيلسريعأقل حتى التسخينمحمول وبناءات متوقعة؛ الحلقات الساخنة أغلى
ترجمة AOTقبل بدء العمليةيعتمد على حجم الثنائيعالية للشفرة الأصليةبناء أطول؛ شحن لكل CPU/OS؛ أخطاء مبكرة
JITأول تشغيلات تفسير؛ الشفرة الساخنة تُترجم لاحقاًسريع بما يكفي للتصفحعالية جداً على مسارات مستقرةعبء profiling؛ deopt حين تنكسر الافتراضات

الاختيار بينها نادراً ما يكون أيديولوجياً. ألعاب الجوال وأدوات CLI تفضّل ثنائيات AOT بلا مفاجآت VM. خوادم Node أو JVM طويلة العمر توزّع تكلفة تسخين JIT على وقت التشغيل. السكربتات القصيرة وجلسات REPL تستفيد من التفسير دون انتظار التحسين. دورك كمطوّر أن تعرف أي خط أنابيب يستخدم وقت تشغيلك حتى تفسّر الملفات الشخصية وتتبّعات المكدس بشكل صحيح — لا حفظ كل علامة محرّك.

البايتكود والآلات الافتراضية

البايتكود مجموعة تعليمات مدمجة لآلة افتراضية — غير مرتبطة بمعمارية CPU واحدة. ملفات Java .class وPython .pyc ووحدات WebAssembly تستهدف VMs تفرض أمان الذاكرة أو قواعد sandbox. الـ VM يتولى اتفاقيات الاستدعاء والاستثناءات، وأحياناً JIT إلى شفرة أصلية للحلقات الساخنة.

الـ VM بين مصدرك ونظام التشغيل: يمكنه التحقق من البايتكود قبل التنفيذ (Java)، وفرض قواعد sandbox (WASM في المتصفح)، أو إدراج hooks للمصحّحات والـ profilers. هذا التوسيط يكلّف CPU لكنه يشتري قابلية النقل — نفس .jar يعمل على ARM وx86 — ونماذج أخطاء موحّدة مثل استثناءات Java بدلاً من segfaults من مؤشرات سيئة. حين تربط مصحّحاً بـ Node أو Python، غالباً توقف الـ VM بين تعليمات بايتكود، ثم تعود إلى أسطر المصدر عبر بيانات debug أُنتجت وقت الترجمة.

ترجمة JIT في JavaScript الحديث

محرّكات المتصفح وNode.js تحلّل JavaScript وتبني هياكل داخلية وتنفّذ بسرعة عبر مفسّرات. الـ profiling يجد الدوال الساخنة؛ مترجمات JIT (غالباً متعددة الطبقات) تصدر شفرة آلة محسّنة متخصّصة للأنواع الملحوظة. تحدث deoptimization حين تنكسر الافتراضات — لذلك مواقع الاستدعاء monomorphic أسرع من أنواع تتغيّر باستمرار في حلقات ضيقة.

JIT متعدد الطبقات يبدأ في طبقة baseline رخيصة، ثم يرقّي الدوال التي تُشغَّل كثيراً إلى مترجمات محسّنة تتخصّص للأعداد الصحيحة والـ doubles أو أشكال الكائنات الملحوظة فعلاً عند مواقع الاستدعاء. حين ينقلب نوع خاصية من number إلى string، قد يخرج المحرّك إلى مسار أبطأ ويعيد الـ profiling. لذلك micro-benchmarks تكذب: دالة سريعة معزولة قد تُ deopt داخل تطبيق أكبر حيث تتنوّع الأنواع. وحدات WASM تتخطى جزءاً من هذه الرقصة بأن تصل مُترجمة مسبقاً بأنواع صريحة، على حساب toolchain مختلف.

الذاكرة: المكدس مقابل الكومة

تخيّل المكدس ورقة مسودّة لسلسلة الاستدعاء الحالية، والكومة لوحة إعلانات تعيش فيها الكائنات حتى لا يشير إليها شيء. حين main تستدعي fetchUser التي تستدعي parseJson، كل استدعاء يدفع إطاراً: متغيرات محلية مثل id وbuffers مؤقتة في ذلك الإطار وتختفي حين تعود الدالة. حين parseJson تُرجع كائناً جديداً، ذاكرته من الكومة لأن المستدعي ما زال يحتاجه بعد أن يُزال إطار parseJson.

  • مكدس الاستدعاء يحمل سجلات التفعيل: متغيرات محلية، عناوين عودة، وإطارات مكدس لكل استدعاء دالة.
  • Stack overflow يعني تكراراً عميقاً جداً أو استدعاءات متبادلة جامحة — لا «متغيرات كثيرة» بشكل مجرد.
  • الكومة تحمل كائنات بعمر ديناميكي: مصفوفات وclosures وأي شيء يعيش أطول من إطار واحد.
  • اللغات تختلف: Rust يتتبّع الملكية وقت الترجمة؛ C يتوقع free يدوياً؛ Java وJS يستخدمان garbage collectors.
javascript
function total(items) {
  let sum = 0;           // stack: primitive in this frame
  for (const x of items) {
    sum += x.price;      // stack: loop locals
  }
  return { sum, count: items.length }; // heap: new object outlives this frame
}
const cart = [{ price: 9 }, { price: 4 }];
const result = total(cart); // stack: result reference; heap: { sum, count }

جمع القمامة

مجمّعات القمامة التتبّعية تجد الكائنات القابلة للوصول من الجذور (عالمية، مكدسات، سجلات) وتسترد الباقي. التوقفات تعتمد على حجم الكومة وخوارزمية المجمّع وما إذا كان وقت التشغيل يستخدم جمعاً generational أو concurrent. تسريبات الذاكرة في لغات GC ما زالت تحدث حين تحتفظ بمراجع لكائنات لم تعد تحتاجها — GC يحرّر غير القابل للوصول، لا المنسي.

نظام التشغيل والمعالج

تعليمات الآلة تحمّل قيماً إلى السجلات، وتفرع، وتستدعي مكتبات مشتركة، وتصدر syscalls لـ I/O ملفات ومنافذ شبكة وmemory mapping. النواة تُجدول threads وتفرض الصلاحيات وتتعامل مع page faults حين تمتلئ RAM الفيزيائية. وقت تشغيل لغتك فوق ذلك — لكن الـ profiling ما زال يظهر ازدحام syscalls والأقفال حين «يشعر» الكود بالبطء.

التصحيح عبر خط الأنابيب

  • تمييز الصياغة ينتهي حيث يبدأ المحلّل — الأخطاء ثابتة (static).
  • نقاط التوقف والتصحيح خطوة بخطوة تُربَط بالبايتكود أو شفرة الآلة مع معلومات debug.
  • console.log والتسجيل المنظّم يراقبان حالة وقت التشغيل دون إيقاف الزمن.
  • إعادة إنتاج أخطاء الإنتاج غالباً تحتاج نفس flags البناء ومتغيرات البيئة وأشكال البيانات — لا ملف المصدر وحده.

أدوات المراقبة عند حدود وقت التشغيل: profilers تعيّن عينات من المكدسات (إطارات مكدس مجدداً)، tracers يتتبّعون syscalls، ولقطات الكومة تعدّ كائنات الكومة الحية. حين الإنتاج يشحن bundles مُصغّرة فقط، فعّل source maps في متتبّع الأخطاء لتتبّعات رمزية؛ وإلا تصحّح أسماء مُولَّدة مثل n وt بدلاً من fetchUser وparseJson. التطوير المحلي بلا تصغير يخفي هذه الفجوة حتى أول نشر.

واقع الإنتاج

شحن الشفرة يضيف bundlers وminifiers وtree-shaking وإعداداً حسب البيئة. أوقات تشغيل الخادم تضيف مديري عمليات وحاويات وتوسّعاً أفقياً — كل instance ينفّذ نفس خط الأنابيب بشكل مستقل. شفرة العميل تضيف تنزيلاً وتحليلاً وترجمة وميزانيات تنفيذ على main thread، لذلك مقالات المتصفح والعميل مقابل الخادم لدينا تكمّل هذا المقال.

Source maps والتصغير

Minifiers يعيدون تسمية المتغيرات إلى حرف واحد ويحذفون المسافات لتنزّل bundles أسرع؛ المعالج ما زال ينفّذ JavaScript صالحاً، لكن أرقام الأسطر في الأخطاء لم تعد تطابق محرّرك. Source maps ملفات sidecar (أو تعليقات inline) تربط مواضع الـ bundle بالـ TypeScript أو JSX الأصلي. DevTools وبعض مجمّعات السجلات تستهلكها حين تنشر maps مع الأصول — أو ترفعها إلى متتبّعات أخطاء. بلا maps، فشل عند bundle.js:1:48291 حالة تنفيذ حقيقية يجب فكها؛ مع maps ترى نفس تتبّع المكدس كما في التطوير المحلي. لا تنشر source maps على URLs عامة إن كانت تضم المصدر كاملاً ما لم تقصد ذلك؛ كثير من الفرق تقيّد maps على خوادم رموز داخلية.

ما يستحق التذكّر

  • نص المصدر يمر بـ lex وparse وanalysis قبل أي عمل CPU؛ الأخطاء الثابتة وأخطاء وقت التشغيل (runtime) تحدث في مراحل مختلفة.
  • المفسّرات وVMs البايتكود وAOT وJIT نقاط على طيف — معظم اللغات التي تستخدمها تجمع أكثر من واحد.
  • إطارات المكدس مساحة مسودّة لكل استدعاء؛ كائنات الكومة تبقى عبر الاستدعاءات حتى يستردها وقت التشغيل أو يحرّرها.
  • تحويلات الإنتاج (minify، bundle، optimize) تغيّر أسماء الملفات وأرقام الأسطر ما لم تسد source maps الفجوة.
  • الـ profiling والتسجيل يراقبان وقت التشغيل الذي نفّذ فعلاً، لا الملف الذي تمنيت أن يُنفَّذ.

جرّب json-formatter وregex-tester من SmartKitio في المتصفح: يعملان في نفس VM JavaScript وخط JIT الذي يصفه هذا المقال، فتبقى الحمولات الملصوقة على جهازك بينما تستكشف التحليل والمطابقة وسلوك وقت التشغيل دون نشر شفرة.

تعمّق أعمق

  1. Crafting Interpreters (book)
  2. MDN — JavaScript engine fundamentals
  3. Aho, Lam, Sethi, Ullman — Compilers: Principles, Techniques, and Tools

مقالات ذات صلة