Skip to content

كيف تُضافُ قاعدةُ نحوٍ جديدة

وصفةٌ لمن يريدُ إضافةَ إنتاجٍ نحويٍّ جديدٍ إلى اللغة. والترتيبُ ههنا سببيٌّ لا ذوقيّ: كلُّ خطوةٍ يحرسُها ما بعدَها، وتقديمُ الشفرةِ على القاعدةِ يُنتِجُ سلوكًا بلا سند.

القاعدةُ الحاكمة

@expected بلا قاعدةٍ في language-truth/ رأيٌ لا عقد. تُكتَبُ القاعدةُ أوّلًا، ثمّ يُطلَبُ التنفيذ. ولا يُشتَقُّ توقُّعٌ من مُخرَجِ مُنفِّذٍ أبدًا.

١ · إن لزمتْك كلمةٌ جديدة، فالكتالوجُ أوّلًا

language-truth/keywords.yaml هو المكانُ الوحيدُ الذي تُعلَنُ فيه كلمةٌ محجوزةٌ ورمزُها الطرفيّ:

yaml
- { id: "KW-RES-0NN", subcategory: "control_flow", since: "1.x.0",
    word: "كلمتك", tokenType: KEYWORD_YOURS, english: yours,
    roles: [block_opener] }

roles يُقرَأ، فلا يُترَكُ اعتباطًا. ومن هذا الملفِّ يُولَّدُ shared/lexer/generated/keywords_generated.{h,cpp} — لا تُحرَّرْ يدًا.

٢ · أعلِنِ القاعدةَ في ملفِّ طبقتِها

الطبقاتُ ثمانٍ: 00_program · 10_statements · 20_declarations · 30_oop · 40_expressions · 50_patterns · 60_advanced · 70_lexical. والمدخلُ الأدنى:

yaml
- id: gr.stmt.yours                       # المعرّفُ = اسمُ مجلَّدِ المرساةِ لاحقًا
  lhs: { nonterminal: YoursStatement, name_ar: "جملة كذا", name_en: yours_statement }
  ebnf: "YoursStatement = 'كلمتك' Expression ;"
  alternatives:
    - symbols:
        - { kind: terminal,    ref: KEYWORD_YOURS, value: "كلمتك" }
        - { kind: nonterminal, ref: gr.expr.expression }
  references: { keywords: ["KW-RES-0NN"] }
  maps_to:
    - { file: shared/parser/src/statements/parser_statements.cpp,
        function: "ParserCore::parseYoursStmt" }
  ast_node: YoursStmt
  description_ar: "…"
  since: "1.x.0"
  status: experimental                     # stable حين يُبرهَنَ لا حين يُرجى
  conformance:
    compiler_optional: false
    test_budget: { basic: 3, edge: 3, negative: 1, interaction: 2 }

ثلاثةُ تنبيهاتٍ مقيسة:

  • references.keywords يُحَلُّ فعلًا. رمزٌ طرفيٌّ لا وجودَ له في الكتالوجِ يُوقِفُ check_grammar_terminal_refs.py.
  • alternatives[*].symbols[*].ref إلى قاعدةٍ أخرى يبني رسمَ قابليّةِ التركيب الذي تُشتقُّ منه أزواجُ التداخلِ المطلوبة (§٥). فالإحالةُ الناقصةُ تُنقِصُ التغطيةَ المطلوبةَ منك بصمت.
  • لا تكتبْ # غيرَ مقتبسٍ في حقلِ وصف. check_yaml_desc_hash.py يمنعُه لأنّه بترٌ صامتٌ لبقيّةِ السطر.

٣ · نفِّذْ في الشفرةِ ثمّ أعِدِ التوليد

نفِّذْ parseYoursStmt في الملفِّ الذي سمّاه maps_to (المدخلُ عقدٌ مقروءٌ لا تعليق)، ثمّ:

bash
python x.py gen                 # يكتبُ المولَّدَ من language-truth/
python x.py build --config Release
python x.py gen --check         # يجبُ أن يخضرّ قبلَ أيِّ إيداع

gen --check يُعيدُ التوليدَ إلى مجلَّدٍ مؤقّتٍ ويقارِنُ بايتاتِه بالمُودَع — فأيُّ فرقٍ بينَ YAML والمُودَعِ يُوقِفُ الدمج. أودِعِ المولَّدَ مع مصدرِه في الإيداعِ نفسِه.

٤ · أنشئ مجلَّدَ المرساة — واسمُه المعرّفُ حرفًا

tests/behavior/rules_matrix/10_statements/gr.stmt.yours/
├── basic/    001_….ص
├── edge/     001_….ص
└── negative/ 001_….ص

هذه الخطوةُ ليست اختياريّة

سقفُ «مُعلَنٌ بلا مرساة» في check_anchor_integrity.py هو ٢ ولا يُرفَع. فقاعدةٌ جديدةٌ بلا بذرةٍ واحدةٍ تحملُ وسمَها ترفعُ العددَ إلى ٣ فيُوقَفُ الدمج. وهذا هو المجسُّ ⑥ في سجلِّ عيارِ ذلك الحارس، مُثبَتًا بالحقنِ لا مفترَضًا.

وكلُّ بذرةٍ تحملُ في رأسِها عقدًا يُحاكَمُ:

sad
# @rule: gr.stmt.yours
# @description: أقصرُ صورةٍ صحيحة
# @priority: P1
# @requires: اطبع_سطر
# @expected: ن

للبذرةِ السالبةِ استعملْ @expect_error برمزٍ من الكتالوج:

sad
# @rule: gr.stmt.yours
# @expect_error: SEM013

رمزٌ لا وجودَ له في language-truth/errors/ يُحمِّرُ حارسَ المرساة — مُثبَتٌ بالمجسِّ ④ الذي يحقنُ SEM999 ويتحقّقُ من ظهورِ err.SEM999.

لا تخترعْ رمزَ خطأ

إعلانُ رمزٍ جديدٍ قرارُ مصدرِ حقيقةٍ لا ترقيعُ باني. وخمسةُ رموزٍ في الكتالوجِ وسمُها retired لأنّ تعريفَها كان المفسّرَ المحذوف (INT008 · INT009 · INT010 · RUN059 · SEM027) — لا تُوجَّهُ إليها بذرة.

٥ · غطِّ أزواجَ التداخل

الأزواجُ المطلوبةُ مشتقّةٌ من إحالاتِ alternatives، فبذرةُ تداخلٍ تُوضَعُ في _interactions/<موضوع>/ وتَسرُدُ كلَّ القواعدِ التي تمسُّها:

sad
# @rule: gr.stmt.yours, gr.expr.expression
bash
python scripts/codegen/check_grammar_conformance.py     # يطبعُ العجزَ صفًّا صفًّا

ما يُوقِفُ الدمجَ فعلًا

ميزانيّةُ الفئاتِ وتغطيةُ الأزواجِ تُقاسانِ وتُنشَرانِ ولا تُوقفانِ الدمجَ اليوم: check_grammar_conformance.py يُخفِقُ برمزِ ١ وليس في x.py gen --check. المُشغَّلُ في البوّابةِ هو gen_rules_matrix.py --check — سلامةٌ مرجعيّةٌ فحسب — مع حارسِ المرساةِ وحارسِ عقدِ البذور. لا تبنِ افتراضًا على أنّ نقصَ الميزانيّةِ سيُوقِفُك.

٦ · اشتقَّ التنزيل

bash
python scripts/codegen/gen_grammar_lowers_to.py \
  --compiler build/bin/Release/sad-build.exe --recompute
python scripts/codegen/check_lowers_to.py

الاشتقاقُ يُترجِمُ بذورَك بـ--أظهر-sir ويجمعُ أوپكوداتِها. فإن لم تُترجَمْ بذرةٌ واحدةٌ منها بنجاحٍ وُسِمَت القاعدةُ not_derived صراحةً — لا تُترَكُ خانتُها فارغةً موحيةً بالنجاح. ولا تُحرِّرْ lowers_to.yaml بيد: هو مُخرَجُ مولِّد.

٧ · شغِّلْ ما بنيتَ

bash
python tests/runner.py --compiler build/bin/Release/sad-build.exe \
  --dir rules_matrix/10_statements/gr.stmt.yours

خضرةٌ هنا لا تكفي وحدَها: بذرةٌ بلا @expected ولا وسمٍ سالبٍ تُشغَّلُ ولا تُحاكَمُ، وهي مقيسةٌ ومحروسةٌ بسقفٍ نازلٍ في check_seed_contract.py. تحقّقْ أنّ عقدَك مكتوب.

٨ · قائمةُ الإيداع

bash
python x.py gen --check          # المولَّدُ متزامنٌ + حرّاسُ الاتّساقِ كلُّهم
python x.py build --config Release
python x.py test  --config Release
  • [ ] القاعدةُ في ملفِّ طبقتِها، ومعرّفُها فريدٌ ومعناه واضح.
  • [ ] كلُّ رمزٍ طرفيٍّ مُحال إليه موجودٌ في keywords.yaml.
  • [ ] المولَّدُ مُودَعٌ مع مصدرِه.
  • [ ] مجلَّدُ المرساةِ باسمِ المعرّفِ حرفًا، وفيه بذرةٌ واحدةٌ على الأقلّ.
  • [ ] كلُّ بذرةٍ لها @expected أو @expect_error برمزٍ من الكتالوج.
  • [ ] lowers_to.yaml أُعيدَ اشتقاقُه وأُودِع.
  • [ ] status صادقٌ: experimental حتّى تكتملَ الميزانيّةُ ويُبرهَنَ التنزيل.

ما لا تفعلْه

  • لا تضِفْ بذرةً يتيمةً إلى sections/. نسبةُ @skip_compiler في rules_matrix — حيثُ المرساةُ قاعدة٥ من ٣٬٠٥٥ = ٠٫١٦٪؛ وفي sections — حيثُ المرساةُ موضوعٌ — ٦٥ من ١٬٢٧٧ = ٥٫٠٩٪. نحوَ ٣٢ ضعفًا، والمجموعةُ الأنظفُ أكبرُ بمرّتين ونصف. الدَّينُ يتراكمُ حيث لا قاعدةَ تحكم.

    bash
    python - <<'EOF'
    import glob, re
    for d in ("rules_matrix", "sections"):
        fs = glob.glob(f"tests/behavior/{d}/**/*.ص", recursive=True)
        sk = sum(bool(re.search(r"^#\s*@skip_compiler",
                                open(f, encoding="utf-8-sig").read(3000), re.M))
                 for f in fs)
        print(d, len(fs), sk, round(100 * sk / len(fs), 2))
    EOF
  • لا تكتبْ قاعدةً تصفُ ما يجبُ رفضُه. النحوُ يعلنُ ما يُقبَل. أمّا الرفضُ فيُعلَنُ رمزًا في كتالوجِ الأخطاء، وذلك قرارٌ مستقلٌّ عن إضافةِ الإنتاج.

  • لا تُحرِّرْ ملفًّا صدرُه «مُولَّد آليًّا». ستُعادُ كتابتُه، وسيحمرُّ gen --check قبلَ ذلك.

مُرخَّص بموجب رخصة MIT