نحو (زبان‌‌های برنامه نویسی)

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

نحو یک زبان فرم سطحی آن را تعریف می‌کند. زبان‌های کامپیوتری مبتنی بر متن بر اساس توالی کاراکترها هستند، در حالی که زبان‌های برنامه‌نویسی بصری بر اساس چیدمان فضایی و ارتباط بین نمادها (که می‌توانند متنی یا گرافیکی باشند) ساخته می‌شوند. اسنادی که از نظر نحوی نامعتبر هستند، دارای خطای نحوی محسوب می‌شوند. در طراحی نحو یک زبان، طراح ممکن است با نوشتن مثال‌هایی از رشته‌های مجاز و غیرمجاز شروع کند و سپس سعی کند قواعد کلی را از این مثال‌ها استخراج نماید.

بنابراین، نحو به فرم کد اشاره دارد و در مقابل آن، معناشناسی یا سمانتیک به معنای کد مربوط می‌شود. در پردازش زبان‌های کامپیوتری، پردازش معنایی معمولاً پس از پردازش نحوی انجام می‌شود؛ با این حال، در برخی موارد، پردازش معنایی برای تکمیل تحلیل نحوی ضروری است و این دو به صورت هم‌زمان یا موازی انجام می‌شوند. در یک کامپایلر، تحلیل نحوی بخش فرانت‌اند را تشکیل می‌دهد، در حالی که تحلیل معنایی بخش بک‌اند (و در صورت تمایز، بخش میانی) را شامل می‌شود.

## سطوح نحو
نحو در زبان‌های برنامه‌نویسی معمولاً به سه سطح تقسیم می‌شود:
* واژه‌ها – سطح واژگانی (lexical)، که تعیین می‌کند چگونه نویسه‌ها (کاراکترها) توکن‌ها را تشکیل می‌دهند؛
* عبارات – سطح دستوری (grammar)، به‌معنای محدود، که تعیین می‌کند چگونه توکن‌ها عبارات را تشکیل می‌دهند؛
* مفهوم – که تعیین می‌کند اشیا یا نام متغیرها به چه چیزی اشاره دارند، آیا انواع داده معتبر هستند و…

این تفکیک باعث ایجاد ماژولاریتی (ساختار پیمانه‌ای) می‌شود، به‌طوری که امکان توصیف و پردازش هر سطح به‌صورت جداگانه و اغلب مستقل فراهم می‌گردد.

ابتدا، یک لکسر (lexer) توالی خطی کاراکترها را به توالی خطی توکن‌ها تبدیل می‌کند؛ این فرایند با عنوان «تحلیل واژگانی» یا لکسینگ (lexing) شناخته می‌شود.

سپس، پارسِر (parser) توالی خطی توکن‌ها را به یک درخت نحوی سلسله مراتبی تبدیل می‌کند؛ این مرحله به‌طور خاص «پارسینگ» (parsing) نامیده می‌شود. این مرحله اطمینان حاصل می‌کند که توالی توکن‌ها با گرامرهای رسمی زبان برنامه‌نویسی مطابقت دارد. مرحلهٔ پارسینگ خود می‌تواند به دو بخش تقسیم شود: درخت پارس یا درخت نحو صوری (Concrete Syntax Tree) که توسط گرامر تعیین می‌شود ولی معمولاً برای استفاده عملی بسیار جزئی و پیچیده است، و درخت نحو انتزاعی (Abstract Syntax Tree - AST) که ساختار را ساده‌تر و قابل استفاده می‌سازد. مراحل AST و تحلیل زمینه‌ای می‌توانند به عنوان نوعی تحلیل معنایی در نظر گرفته شوند، زیرا آن‌ها به نحو معنا و تفسیر می‌افزایند؛ یا می‌توان آن‌ها را به عنوان پیاده‌سازی‌های غیررسمی و دستی از قواعد نحوی دانست که توصیف یا پیاده‌سازی رسمی‌شان دشوار یا ناخوشایند است.

در گام سوم، تحلیل زمینه‌ای (Contextual Analysis) به شناسایی و تفسیر نام‌ها و بررسی انواع داده می‌پردازد. این ساختار پیمانه‌ای گاهی امکان‌پذیر است، اما در بسیاری از زبان‌های برنامه‌نویسی در دنیای واقعی، یک مرحلهٔ قبل به مرحله بعدی وابسته است – برای مثال، در زبان C، چیزی به‌نام «lexer hack» وجود دارد، چون توکنیزاسیون بسته به زمینه (context) متفاوت است. حتی در چنین مواردی نیز تحلیل نحوی غالباً به‌عنوان تقریبی از این مدل ایده‌آل در نظر گرفته می‌شود.

این سطوح معمولاً با سطوح موجود در سلسله‌مراتب چامسکی (Chomsky hierarchy) متناظر هستند: واژه‌ها در یک زبان منظم (regular language) تعریف می‌شوند که توسط گرامر واژگانی مشخص می‌شود، که یک گرامر نوع-۳ (Type-3) بوده و معمولاً با عبارات منظم (regular expressions) تعریف می‌گردد. عبارات در یک زبان مستقل از زمینه (Context-Free Language - CFL) تعریف می‌شوند، که اغلب از نوع مستقل از زمینهٔ قطعی (Deterministic Context-Free Language - DCFL) هستند. این‌ها توسط گرامر ساختار عبارتی تعریف می‌شوند، که گرامر نوع-۲ (Type-2) بوده و معمولاً با فرم بکوس-نائر (Backus–Naur Form - BNF) بیان می‌شوند. گرامرهای عبارتی معمولاً در قالب گرامرهایی بسیار محدودتر از گرامرهای مستقل از زمینهٔ کامل تعریف می‌شوند، تا تحلیل آن‌ها ساده‌تر گردد؛ در حالی‌که پارسرهای LR می‌توانند هر زبان DCFL را در زمان خطی پارس کنند، پارسرهای ساده‌تر مانند LALR و حتی LL کارایی بیشتری دارند، اما تنها گرامرهایی را می‌توانند پردازش کنند که قواعد تولیدی آن‌ها محدودتر باشد. از نظر تئوری، ساختار زمینه‌ای را می‌توان با گرامرهای وابسته به زمینه (context-sensitive grammar) توصیف کرد و به صورت خودکار از طریق روش‌هایی مانند گرامرهای وابسته به ویژگی (attribute grammars) تحلیل نمود؛ اما در عمل، این مرحله به‌صورت دستی انجام می‌شود، از طریق قوانینی برای نام‌گذاری (name resolution) و بررسی نوع (type checking)، و با استفاده از جدول نمادها (symbol table) که نام‌ها و انواع داده را در هر حوزه (scope) ذخیره می‌کند.

ابزارهایی وجود دارند که به طور خودکار از یک مشخصه واژگانی (نوشته شده با عبارات منظم) یک لکسر و از یک گرامر عبارتی (نوشته‌شده در BNF) یک پارسِر تولید می‌کنند: این امکان را فراهم می‌سازد که از برنامه‌نویسی اعلامی (declarative programming) استفاده شود، به‌جای نیاز به برنامه‌نویسی رویه‌ای یا تابعی. مثالی شاخص از این ابزارها، جفت lex-yacc است. این ابزارها به صورت خودکار یک درخت نحو صوری تولید می‌کنند؛ سپس نویسندهٔ پارسر باید به‌صورت دستی کدی بنویسد که نحوه تبدیل این ساختار به درخت نحو انتزاعی را مشخص کند. تحلیل زمینه‌ای نیز معمولاً به‌صورت دستی پیاده‌سازی می‌شود. با وجود این ابزارهای خودکار، در بسیاری از موارد، تحلیل نحوی به‌صورت دستی پیاده‌سازی می‌شود، به دلایل مختلف – مثلاً ممکن است ساختار عبارتی مستقل از زمینه نباشد، یا پیاده‌سازی جایگزین عملکرد بهتری داشته باشد یا پیام‌های خطا را بهتر مدیریت کند، یا امکان تغییر گرامر را ساده‌تر سازد. پارسِرها اغلب در زبان‌های تابعی مانند Haskell، یا زبان‌های اسکریپتی مانند Python یا Perl، یا در C و ++C نوشته می‌شوند.

### مثال‌هایی از خطاها
مقالهٔ اصلی: خطای نحوی (Syntax error)

برای مثال، `(add 1 1)` یک برنامهٔ معتبر از نظر نحوی در زبان Lisp است (به‌فرض اینکه تابع `add` وجود داشته باشد، در غیر این صورت تحلیل نام شکست می‌خورد) که عدد ۱ و ۱ را با هم جمع می‌کند. با این حال، مثال‌های زیر نامعتبر هستند:
 `(_ 1 1)    خطای واژگانی (lexical): نماد '_' معتبر نیست
 (add 1 1   خطای نحوی (parsing): پرانتز بسته ')' فراموش شده است`
لکسر قادر به شناسایی خطای اول نیست – تنها چیزی که می‌داند این است که پس از تولید توکن `LEFT_PAREN` یعنی `'('`، باقی‌ماندهٔ برنامه نامعتبر است، زیرا هیچ قاعدهٔ واژگانی با `'_'` شروع نمی‌شود.

خطای دوم در مرحلهٔ پارسینگ شناسایی می‌شود: پارسر، بر اساس توکن `'('`، قاعدهٔ تولیدی مربوط به «فهرست» (list) را شناسایی کرده (به عنوان تنها تطبیق موجود)، و بنابراین می‌تواند پیام خطا ارائه کند؛ البته به طور کلی این ممکن است مبهم باشد.

خطاهای نوع (type errors) و خطاهای متغیرهای تعریف نشده گاهی وقتی در زمان کامپایل شناسایی می‌شوند، به‌عنوان خطای نحوی در نظر گرفته می‌شوند (که معمولاً در زبان‌های با نوع دهی قوی اتفاق می‌افتد)، هرچند معمول است که این نوع خطاها به‌جای آن به عنوان خطای معنایی دسته‌بندی شوند.

برای مثال، کد زیر در زبان Python:
 `'a' + 1`
دارای خطای نوع است زیرا یک رشتهٔ متنی را با یک عدد صحیح جمع می‌کند.

چنین خطاهای نوعی را می‌توان در زمان کامپایل شناسایی کرد: اگر کامپایلر قواعد مجزایی داشته باشد که اجازه دهد `عدد + عدد` ولی نه `رشته + عدد`، در مرحلهٔ پارسینگ (تحلیل عبارتی) شناسایی می‌شوند.

هرچند معمول‌تر است که کامپایلر از قاعده‌ای استفاده کند که تمام عبارات به‌شکل `شناسه یا مقدار + شناسه یا مقدار` را بپذیرد، و در این صورت، خطا در مرحلهٔ **تحلیل زمینه‌ای** (contextual analysis) و در زمان بررسی نوع (type checking) شناسایی خواهد شد.

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

در زبان‌های با نوع‌دهی پویا (dynamically typed)، که نوع تنها در زمان اجرا قابل تعیین است، بسیاری از خطاهای نوع تنها در زمان اجرا قابل شناسایی‌اند.

برای مثال، کد زیر در Python:
 `a + b`
در سطح نحوی (عبارتی) معتبر است، اما درستی نوع متغیرهای `a` و `b` تنها در زمان اجرا مشخص می‌شود، چون در Python، متغیرها نوع ندارند، بلکه مقدارها دارای نوع هستند. در حالی که دربارهٔ اینکه آیا خطای نوعی که توسط کامپایلر شناسایی می‌شود باید خطای نحوی نامیده شود (یا بهتر است آن را خطای معنایی ایستا دانست) اختلاف نظر وجود دارد، اما خطاهای نوعی که فقط در زمان اجرای برنامه قابل شناسایی هستند، همواره به عنوان خطاهای معنایی در نظر گرفته می‌شوند، نه نحوی.

## تعریف نحو
نحو در زبان‌های برنامه‌نویسی متنی معمولاً با استفاده از ترکیبی از عبارات منظم (برای ساختار واژگانی) و فرم بکوس–نائر (Backus–Naur form) (که یک فرامل برای ساختار دستوری است) تعریف می‌شود تا به‌صورت استقرایی، دسته‌های نحوی (غیرنهایی‌ها) و نمادهای نهایی را مشخص کند. دسته‌های نحوی توسط قواعدی به نام تولیدها (productions) تعریف می‌شوند که مقادیر مجاز برای یک دستهٔ نحوی خاص را مشخص می‌کنند. نمادهای نهایی (terminal symbols) نویسه‌ها یا رشته‌هایی از نویسه‌ها هستند (برای مثال، کلمات کلیدی مانند `define`، `if`، `let` یا `void`) که برنامه‌های نحوی معتبر از آن‌ها ساخته می‌شوند.

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

یک زبان می‌تواند گرامرهای معادل مختلفی داشته باشد، مانند عبارات منظم معادل (در سطح واژگانی) یا قواعد عبارتی متفاوت که همان زبان را تولید می‌کنند. استفاده از دستهٔ گسترده‌تری از گرامرها – مانند گرامرهای LR – می‌تواند منجر به گرامرهایی کوتاه‌تر یا ساده‌تر نسبت به دسته‌های محدودتر (مانند گرامرهای LL) شود که ممکن است نیازمند گرامرهای طولانی‌تری با قواعد بیشتر باشند. گرامرهای عبارتی مختلف ولی معادل، ممکن است درخت‌های نحوی متفاوتی تولید کنند، گرچه زبان زیربنایی (مجموعه اسناد معتبر) یکسان باقی می‌ماند.

### مثال: S-Expressions در Lisp
در ادامه یک گرامر ساده با استفاده از نشانه‌گذاری عبارات منظم و فرم نائر - بسط‌یافته (Extended BNF) تعریف شده است. این گرامر نحو S-Expressions را توصیف می‌کند؛ نحوی داده‌ای که در زبان برنامه‌نویسی Lisp استفاده می‌شود و تولیدهایی برای دسته‌های نحوی expression, atom, number, symbol و list تعریف می‌کند:
 `expression = atom   | list
 atom       = number | symbol
 number     = [+-]?['0'-'9']+
 symbol     = ['A'-'Z']['A'-'Z0'-'9'].*
 list       = '(', expression*, ')'`
این گرامر موارد زیر را مشخص می‌کند:
* یک expression یا یک atom است یا یک list؛
* یک atom یا یک number است یا یک symbol؛
* یک number دنباله‌ای بدون وقفه از یک یا چند رقم ده‌دهی است، که به‌طور اختیاری می‌تواند با علامت مثبت یا منفی شروع شود؛
* یک symbol یک حرف است که بعد از آن صفر یا تعداد بیشتری کاراکتر می‌آید (به‌جز فاصلهٔ سفید)؛
* یک list یک جفت پرانتز باز و بسته است که درون آن صفر یا چند expression قرار دارد.

در اینجا، رقم‌های ده‌دهی، حروف بزرگ و کوچک انگلیسی و پرانتزها جزو نمادهای نهایی (ترمینال) محسوب می‌شوند.

نمونه‌هایی از توالی‌های توکن که طبق این گرامر به درستی شکل گرفته‌اند عبارت‌اند از:
 `'12345', '()', '(A B C232 (1))'`

### گرامرهای پیچیده
گرامری که برای تعریف یک زبان برنامه‌نویسی لازم است، را می‌توان با جایگاه آن در سلسله مراتب چامسکی (Chomsky hierarchy) طبقه‌بندی کرد. گرامر عبارتی بیشتر زبان‌های برنامه‌نویسی را می‌توان با یک گرامر نوع-۲ (Type-2) مشخص کرد، یعنی آن‌ها گرامرهای مستقل از زمینه (context-free grammars) هستند، اگرچه نحو کلی زبان وابسته به زمینه است (به دلیل اعلان متغیرها و حوزه‌های تودرتو)، و بنابراین نوع-۱ محسوب می‌شود. با این حال، استثنائاتی نیز وجود دارد و در برخی زبان‌ها، گرامر عبارتی از نوع صفر (Type-0) است، یعنی توانای تورینگ (Turing-complete).

در برخی زبان‌ها مانند Perl و Lisp، تعریف (یا پیاده‌سازی) زبان به گونه‌ای است که امکان اجرای سازه‌هایی در مرحلهٔ پارسینگ را فراهم می‌کند. علاوه بر این، این زبان‌ها دارای سازه‌هایی هستند که به برنامه‌نویس اجازه می‌دهند رفتار پارسر را تغییر دهد. این ترکیب به طور مؤثری مرز میان پارس کردن و اجرا را محو می‌کند و موجب می‌شود تحلیل نحوی در این زبان‌ها مسئله‌ای حل‌ناپذیر (undecidable) باشد؛ به این معنا که ممکن است مرحلهٔ پارسینگ به پایان نرسد. برای مثال، در Perl امکان اجرای کد در حین پارس کردن با استفاده از دستور `BEGIN` وجود دارد، و پروتوتایپ توابع در Perl می‌توانند تفسیر نحوی کد باقی‌مانده را تغییر دهند، و حتی ممکن است اعتبار نحوی آن را نیز تحت تأثیر قرار دهند. به‌صورت محاوره‌ای، این حالت با عبارت مشهور «فقط Perl می‌تونه Perl رو پارس کنه» توصیف می‌شود (زیرا کد باید در حین پارس اجرا شود و می‌تواند گرامر را تغییر دهد)، یا به شکل قوی‌تر «حتی خود Perl هم نمی‌تونه Perl رو پارس کند» (چون از نظر تئوری این فرایند حل‌ناپذیر است). به‌طور مشابه، ماکروهای Lisp که با دستور `defmacro` معرفی می‌شوند، نیز در زمان پارس شدن اجرا می‌شوند، به این معنا که یک کامپایلر Lisp باید دارای یک سیستم اجرای کامل Lisp در هنگام پارسینگ باشد. در مقابل، ماکروهای زبان C صرفاً جایگزینی رشته‌ای هستند و نیازی به اجرای کد ندارند.

## نحو در مقابل معناشناسی
نحو (syntax) یک زبان، شکل و قالب برنامه‌ای معتبر را توصیف می‌کند، اما هیچ اطلاعاتی دربارهٔ معنای آن برنامه یا نتیجهٔ اجرای آن ارائه نمی‌دهد.

معنایی که به ترکیبی از نمادها داده می‌شود، توسط معناشناسی (semantics) مشخص می‌شود (خواه به‌صورت رسمی و ریاضی، یا به صورت کدنویسی شده در پیاده‌سازی مرجع). نحو باید ابتدا به درستی برقرار شود تا معناشناسی بتواند از آن برداشت معنایی داشته باشد. همهٔ برنامه‌های نحوی معتبر، از نظر معنایی نیز معتبر نیستند. بسیاری از برنامه‌هایی که از نظر نحوی درست هستند، با توجه به قوانین زبان، همچنان نادرست ساختاری (ill-formed) محسوب می‌شوند؛ و ممکن است (بسته به مشخصات زبان و صحت پیاده‌سازی آن) در زمان ترجمه یا اجرا منجر به خطا شوند. در برخی موارد، چنین برنامه‌هایی ممکن است رفتار نامعین (undefined behavior) داشته باشند. حتی زمانی‌که یک برنامه از نظر نحوی و معنایی در زبان تعریف شده باشد، ممکن است معنایی داشته باشد که با قصد نویسندهٔ برنامه متفاوت باشد.

با استفاده از زبان طبیعی به‌عنوان مثال، ممکن است نتوان به جمله‌ای که از نظر دستوری صحیح است، معنایی نسبت داد، یا جمله‌ای ممکن است درست ساخته شده باشد اما معنایی نادرست را بیان کند:
* «ایده‌های سبزِ بی‌رنگ به شدت می‌خوابند.» و از نظر دستوری درست است، اما هیچ معنای پذیرفته‌شده‌ای ندارد.
* «جان یک مرد مجردِ متأهل است.» از نظر دستوری صحیح است، اما معنایی متناقض دارد و نمی‌تواند درست باشد.

قطعه کد زیر در زبان C از نظر نحوی معتبر است، اما عملیاتی انجام می‌دهد که از نظر معناشناسی تعریف‌نشده است (زیرا اشاره‌گر `p` برابر NULL است، و عملیات `p->real` و `p->im` بی‌معنا هستند):
 `complex *p = NULL;
 complex abs_p = sqrt(p->real * p->real + p->im * p->im);`
مثال ساده‌تری از این موضوع:
 `int x;
 printf("%d", x);`
این کد از نظر نحوی معتبر است، اما از نظر معناشناسی تعریف‌نشده است، زیرا متغیر `x` مقداردهی اولیه نشده است. هرچند که کامپایلرهای برخی زبان‌های برنامه‌نویسی (مانند Java یا C#) می‌توانند چنین خطاهایی را شناسایی کنند، اما این نوع خطاها باید به‌عنوان خطاهای معنایی در نظر گرفته شوند، نه خطاهای نحوی.

## منابع
* 

رده:زبان‌های برنامه‌نویسی
رده:کد منبع