Як мови програмування визначають функції?


28

Як мови програмування визначають і зберігають функції / методи? Я створюю інтерпретовану мову програмування в Ruby, і я намагаюся зрозуміти, як реалізувати декларацію функції.

Моя перша ідея - зберегти вміст декларації на карті. Наприклад, якщо я зробив щось подібне

def a() {
    callSomething();
    x += 5;
}

Тоді я б додав запис до своєї карти:

{
    'a' => 'callSomething(); x += 5;'
}

Проблема в цьому полягає в тому, що він став би рекурсивним, тому що я повинен був би викликати свій parseметод на рядку, який потім зателефонував би parseзнову, коли він зіткнувся doSomething, і тоді мені б в кінцевому підсумку не вистачило місця у стеці.

Отже, як з цим впораються інтерпретовані мови?


О, і це моя перша публікація на Programmers.SE, тому, будь ласка, повідомте мене, якщо я роблю щось не так або це поза темою. :)
Doorknob

Раніше я зберігав їх у вбудованих токенах, а виклики функцій - це просто переходи до певного зміщення (подібно до міток у асамблеї). Ви токенізуєте сценарій? Або кожен раз аналізує рядки?
Саймон Уайтхед

@SimonWhitehead Я розділив рядок на лексеми, а потім розібрав кожен маркер окремо.
Doorknob

3
Якщо ви не знайомі з розробкою та реалізацією мови програмування, ви можете перевірити частину літератури з цього питання. Найпопулярніша - «Книга Драконів»: en.wikipedia.org/wiki/… , але є й інші, більш стислі тексти, які теж дуже хороші. Наприклад, Мови програмування від Аарна Ранта можна безкоштовно отримати тут: bit.ly/15CF6gC .
evilcandybag

1
@ddyer Дякую! Я попросив перекладача на різних мовах, і це справді допомогло. :)
Doorknob

Відповіді:


31

Чи правильно я припускаю, що ваша функція "розбору" не тільки аналізує код, але й виконує його одночасно? Якщо ви хотіли зробити це саме так, замість того, щоб зберігати вміст функції у вашій карті, збережіть розташування функції.

Але є кращий спосіб. Це вимагає трохи більше зусиль наперед, але це дає набагато кращі результати, оскільки складність збільшується: використовуйте абстрактне синтаксичне дерево.

Основна ідея полягає в тому, що ви аналізуєте код лише раз, коли-небудь. Тоді у вас є набір типів даних, що представляють операції та значення, і ви складаєте з них дерево таким чином:

def a() {
    callSomething();
    x += 5;
}

стає:

Function Definition: [
   Name: a
   ParamList: []
   Code:[
      Call Operation: [
         Routine: callSomething
         ParamList: []
      ]
      Increment Operation: [
         Operand: x
         Value: 5
      ]
   ]
]

(Це лише текстове зображення структури гіпотетичного AST. Фактичне дерево, мабуть, не буде в текстовій формі.) У будь-якому випадку, ви розбираєте свій код в AST, а потім або запускаєте інтерпретатора над AST безпосередньо, або використовувати другий ("генерація коду") переходу для перетворення AST в якусь форму виводу.

Що стосується вашої мови, то, напевно, ви могли б зробити карту, яка відображає назви функцій для функціонування AST, а не імена функцій для функціональних рядків.


Гаразд, але проблема все ж є: вона використовує рекурсію. У кінцевому підсумку у мене залишиться місця для стеку, якщо я це зроблю.
Doorknob

3
@Doorknob: Для чого конкретно використовується рекурсія? Будь-яка блокова структурована мова програмування (яка є кожною сучасною мовою на більш високому рівні, ніж ASM) за своєю суттю є деревною та, таким чином, рекурсивною за своєю суттю. Який конкретний аспект Ви турбуєтесь, щоб отримати переповнення стека?
Мейсон Уілер

1
@Doorknob: Так, це властива будь-якій мові, навіть якщо вона складена до машинного коду. (Стек викликів є проявом такої поведінки.) Я насправді є учасником системи скриптів, яка працює так, як я описав. Приєднуйтесь до мене в чаті на сайті chat.stackexchange.com/rooms/10470/…, і я обговорю деякі методи ефективної інтерпретації та мінімізації впливу на розмір стека. :)
Мейсон Уілер

2
@Doorknob: Тут немає жодної проблеми з рекурсією, оскільки виклик функції в AST посилається на функцію по імені , вона не потребує посилання на фактичну функцію . Якщо ви збираєтеся до машинного коду, то в кінцевому підсумку вам знадобиться адреса функції, саме тому більшість компіляторів роблять кілька проходів. Якщо ви хочете мати однопрохідний компілятор, тоді вам потрібні "переадресації" всіх функцій, щоб компілятор заздалегідь міг призначити адреси. Компілятори байт-кодів навіть не турбуються цим, тремтіння обробляє пошук імен.
Aaronaught

5
@Doorknob: Це справді рекурсивно. І так, якщо ваш стек містить лише 16 записів, ви не зможете проаналізувати (((((((((((((((( x ))))))))))))))))). Насправді стеки можуть бути набагато більшими, а граматична складність реального коду досить обмежена. Звичайно, якщо цей код має бути читабельним для людини.
MSalters

4

Ви не повинні викликати розбір, побачивши callSomething()(я вважаю, що ви мали на увазі, callSomethingа не doSomething). Різниця між aі callSomethingполягає в тому, що один - це визначення методу, а інший - виклик методу.

Коли ви побачите нове визначення, ви хочете зробити перевірки, пов’язані із тим, щоб переконатися, що ви можете додати це визначення, так:

  • Перевірте, чи функція вже не існує з такою ж підписом
  • Переконайтесь, що декларування методу виконується у належному масштабі (тобто чи можуть бути оголошені методи всередині декларацій інших методів?)

Припускаючи, що ці чеки проходять, ви можете додати їх на свою карту і почати перевірку вмісту цього методу.

Коли ви знайдете такий виклик методу callSomething(), вам слід виконати такі перевірки:

  • Чи callSomethingіснує на вашій карті?
  • Чи правильно його називають (кількість аргументів відповідає підпису, який ви знайшли)?
  • Чи є аргументи дійсними (якщо використовуються імена змінних, вони оголошені? Чи можна отримати доступ до цього обсягу?)?
  • Чи можна зателефонувати чомусь звідки ви перебуваєте (це приватний, громадський, захищений?)?

Якщо ви вважаєте, що callSomething()це нормально, то на даний момент, що ви хотіли б зробити насправді, залежить від того, як ви хочете підійти до цього. Власне кажучи, щойно ви знаєте, що в даний момент такий виклик нормальний, ви могли зберегти лише ім'я методу та аргументи, не вникаючи в деталі. Під час запуску програми ви будете викликати метод з аргументами, які ви повинні мати під час виконання.

Якщо ви хочете піти далі, ви можете зберегти не просто рядок, а посилання на фактичний метод. Це було б ефективніше, але якщо вам доведеться керувати пам'яттю, це може заплутатися. Я б рекомендував вам спочатку просто потримати рядок. Пізніше ви можете спробувати оптимізувати.

Зауважте, що це все, припускаючи, що ви лексикували свою програму, а це означає, що ви розпізнали всі маркери у вашій програмі та знаєте, що вони є . Це не означає, що ви знаєте, якщо вони мають сенс разом, що є етапом розбору. Якщо ви ще не знаєте, що таке маркери, пропоную вам спочатку зосередитися на отриманні цієї інформації.

Я сподіваюся, що це допомагає! Ласкаво просимо до програмістів SE!


2

Читаючи вашу публікацію, я помітив у вашому запитання два питання. Найголовніше - як розібратися. Існує багато видів парсерів (наприклад, аналізатор рекурсивного зниження , LR Parsers , Packrat Parsers ) і генератори парсерів (наприклад, GNU bison , ANTLR ), які можна використовувати для переходу текстової програми "рекурсивно" з урахуванням (явного або неявного) граматики.

Друге питання стосується формату зберігання для функцій. Коли ви не робите перекладу , спрямованого на синтаксис , ви створюєте проміжне представлення вашої програми, яке може бути абстрактним синтаксичним деревом або якоюсь спеціальною проміжною мовою, щоб зробити подальшу обробку з ним (компілювати, перетворювати, виконувати, писати далі файл тощо).


1

З загальної точки зору, визначення функції трохи більше, ніж мітка або закладка в коді. Більшість інших операторів циклу, обсягу та умовності схожі; вони стоять за основні команди "стрибок" або "гото" в нижчих рівнях абстракції. Виклик функції в основному зводиться до таких низькорівневих команд комп'ютера:

  • Об'єднайте дані всіх параметрів плюс покажчик на наступну інструкцію поточної функції в структуру, відому як "кадр стека викликів".
  • Натисніть на цей кадр на стек викликів.
  • Перейти до зміщення пам'яті першого рядка коду функції.

Виписка "return" або подібне буде робити наступне:

  • Завантажте значення, яке потрібно повернути в реєстр.
  • Завантажте покажчик абоненту в регістр.
  • Розмістіть поточну рамку стека.
  • Перейти до вказівника абонента.

Отже, функції - це просто абстракції в специфікації мови вищого рівня, які дозволяють людині організовувати код більш ремонтованим та інтуїтивно зрозумілим способом. Коли вони складаються на збірну або проміжну мову (JIL, MSIL, ILX) і, безумовно, коли вони відображаються як машинні коди, майже всі такі абстракції відпадають.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.