Где / как объявить уникальный ключ переменных в компиляторе, написанном на Ocaml? - PullRequest
5 голосов
/ 29 июня 2011

Я пишу компилятор мини-паскаля в Ocaml.Я хотел бы, чтобы мой компилятор принял следующий код, например:

program test;
var
   a,b : boolean;
   n : integer;
begin
   ...
end.

У меня трудности с объявлением переменных (часть, следующая за var).На данный момент тип переменных определяется следующим образом: sib_syntax.ml :

type s_var =
    { s_var_name: string;
      s_var_type: s_type; 
      s_var_uniqueId: s_uniqueId (* key *) }

Где s_var_uniqueId (вместо s_var_name) - уникальный ключ переменных,Мой первый вопрос: где и как я мог бы реализовать механизм генерирования нового идентификатора (фактически, увеличивая самый большой идентификатор на 1) каждый раз, когда я получал новую переменную.Мне интересно, если я должен реализовать это в sib_parser.mly , который, вероятно, включает статическую переменную cur_id и модификацию части binding, опять же не знаю, как реализовать их в .mly.Или я должен реализовать механизм на следующем этапе - interpreter.ml?но в этом случае вопрос состоит в том, как сделать .mly совместимым с типом s_var, что s_var_uniqueId я должен предоставить в части binding?

Другой вопрос касается этой частииз statement в .mly:

id = IDENT COLONEQ e = expression
  { Sc_assign (Sle_var {s_var_name = id; s_var_type = St_void}, e) }

Здесь мне также нужно указать следующий уровень (interpreter.ml), переменную которого я знаю только s_var_name, так что я могЧто касается его s_var_type и s_var_uniqueId здесь?

Может кто-нибудь помочь?Большое спасибо!

Ответы [ 2 ]

5 голосов
/ 29 июня 2011

Первый вопрос, который нужно задать себе, - действительно ли вам нужен уникальный идентификатор. По моему опыту, они почти никогда не нужны или даже полезны. Если вы пытаетесь сделать переменные уникальными через альфа-эквивалентность , то это должно произойти после завершения синтаксического анализа и, вероятно, будет включать некоторую форму индексов Дебрюйна вместо уникальных идентификаторы.

В любом случае, функция, которая возвращает новый целочисленный идентификатор каждый раз, когда она вызывается:

let unique = 
  let last = ref 0 in 
  fun () -> incr last ; !last

let one = unique ()  (* 1 *)
let two = unique ()  (* 2 *)

Итак, вы можете просто назначить { ... ; s_var_uniqueId = unique () } в своих правилах Менгира.

Более важной проблемой, которую вы пытаетесь решить, является проблема привязки переменных . Переменная x определена в одном месте и используется в другом, и вам нужно определить, что это одна и та же переменная в обоих местах. Есть много способов сделать это, один из них - отложить привязку до интерпретатора. Я собираюсь показать вам, как бороться с этим во время разбора.

Во-первых, я собираюсь определить контекст : это набор переменных, который позволяет вам легко извлекать переменную на основе ее имени. Возможно, вы захотите создать его с помощью хеш-таблиц или карт, но для простоты я буду использовать List.assoc здесь.

type s_context = {
  s_ctx_parent : s_context option ;
  s_ctx_bindings : (string * (int * s_type)) list ;
  s_ctx_size : int ;
}

let empty_context parent = {
  s_ctx_parent = parent ;
  s_ctx_bindings = [] ;
  s_ctx_size = 0
}

let bind v_name v_type ctx = 
  try let _ = List.assoc ctx.s_ctx_bindings v_name in
      failwith "Variable is already defined"
  with Not_found -> 
    { ctx with 
      s_ctx_bindings = (v_name, (ctx.s_ctx_size, v_type)) 
        :: ctx.s_ctx_bindings ;
      s_ctx_size = ctx.s_ctx_size + 1 }

let rec find v_name ctx =       
  try 0, List.assoc ctx.s_ctx_bindings v_name
  with Not_found -> 
    match ctx.s_ctx_parent with 
      | Some parent -> let depth, found = find v_name parent in
                       depth + 1, found
      | None -> failwith "Variable is not defined"

Итак, bind добавляет новую переменную в текущий контекст, find ищет переменную в текущем контексте и ее родителей и возвращает как связанные данные, так и глубину, на которой она был найден. Таким образом, вы можете иметь все глобальные переменные в одном контексте, затем все параметры функции в другом контексте, который имеет глобальный контекст в качестве родителя, а затем все локальные переменные в функции (если они у вас будут) в третьем контексте, который имеет основной контекст функции в качестве родителя и т. д.

Так, например, find 'x' ctx вернет что-то вроде 0, (3, St_int), где 0 - это индекс переменной DeBruijn, 3 - это положение переменной в контексте, определяемом индексом DeBruijn, и St_int это тип.

type s_var = {
  s_var_deBruijn: int;
  s_var_type: s_type;
  s_var_pos: int 
}

let find v_name ctx = 
   let deBruijn, (pos, typ) = find v_name ctx in 
   { s_var_deBruijn = deBruijn ;
     s_var_type = typ ;
     s_var_pos = pos }

Конечно, вам нужно, чтобы ваши функции сохраняли свой контекст, и убедитесь, что первым аргументом является переменная в позиции 0 в контексте:

type s_fun =
{ s_fun_name: string;
  s_fun_type: s_type;
  s_fun_params: context; 
  s_fun_body: s_block; }

let context_of_paramlist parent paramlist = 
  List.fold_left 
    (fun ctx (v_name,v_type) -> bind v_name v_type ctx) 
    (empty_context parent)
    paramlist

Затем вы можете изменить свой парсер, чтобы учитывать контекст. Хитрость заключается в том, что вместо возврата объекта, представляющего часть вашего AST, большинство ваших правил будет возвращать функцию, которая принимает контекст в качестве аргумента и возвращает узел AST .

Например:

int_expression:
  (* Constant : ignore the context *)
| c = INT { fun _ -> Se_const (Sc_int c) }
  (* Variable : look for the variable inside the contex *)
| id = IDENT { fun ctx -> Se_var (find id ctx) }
  (* Subexpressions : pass the context to both *)
| e1 = int_expression o = operator e2 = int_expression 
  { fun ctx -> Se_binary (o, e1 ctx, e2 ctx) }
;

Итак, вы просто распространяете контекст "вниз" через выражения. Единственными умными частями являются те, которые создаются при создании новых контекстов (у вас еще нет этого синтаксиса, поэтому я просто добавляю заполнитель):

| function_definition_expression (args, body) 
  { fun ctx -> let ctx = context_of_paramlist (Some ctx) args in
               { s_fun_params = ctx ; 
                 s_fun_body = body ctx } }

Как и глобальный контекст (само правило программы не возвращает функцию, но правило block возвращает, и поэтому контекст создается из глобальных переменных и предоставляется).

prog:
  PROGRAM IDENT SEMICOLON
  globals = variables
  main = block
  DOT
    { let ctx = context_of_paramlist None globals in 
      { globals = ctx;
        main = main ctx } }

Все это значительно упрощает реализацию вашего интерпретатора благодаря индексам Дебрюйна: у вас может быть «стек», в котором хранятся ваши значения (типа value), определенные как:

type stack = value array list 

Затем чтение и запись переменной x так же просто, как:

let read stack x = 
  (List.nth stack x.s_var_deBruijn).(x.s_var_pos)

let write stack x value = 
  (List.nth stack x.s_var_deBruijn).(x.s_var_pos) <- value

Кроме того, поскольку мы убедились, что параметры функции находятся в том же порядке, что и их положение в контексте функции, если вы хотите вызвать функцию f, а ее аргументы хранятся в массиве args, то мы создаем стек так же просто, как:

let inner_stack = args :: stack in
(* Evaluate f.s_fun_body with inner_stack here *)

Но я уверен, что у вас будет гораздо больше вопросов, когда вы начнете работать с вашим интерпретатором;)

2 голосов
/ 29 июня 2011

Как создать генератор глобального идентификатора:

let unique =
  let counter = ref (-1) in
  fun () -> incr counter; !counter

Тест:

# unique ();;
- : int = 0
# unique ();;
- : int = 1

Относительно вашего более общего вопроса проектирования: кажется, что ваше представление данных не совсем точно отражает фазы компилятора. Если вы должны вернуть тип данных с учетом типа (с этим полем s_var_type) после фазы анализа, что-то не так. У вас есть два варианта:

  • разработать более точное представление данных для AST после анализа, которое отличалось бы от AST после типизации и не имело бы этих полей s_var_type. Печатание тогда будет преобразованием из нетипизированного в типизированный AST. Это чистое решение, которое я бы рекомендовал.

  • признайте, что вы должны нарушить семантику представления данных, потому что у вас недостаточно информации на этом этапе, и попытаться быть в мире с идеей возврата мусора, такого как St_void после фазы синтаксического анализа, для восстановить правильную информацию позже. Это менее типизировано (поскольку у вас есть неявное предположение о ваших данных, которое не видно в типе), более прагматично, некрасиво, но иногда необходимо. Я не думаю, что это правильное решение в этом случае, но вы столкнетесь с ситуацией, когда лучше набрать меньше текста.

Я думаю, что конкретный выбор дизайна обработки уникальных идентификаторов зависит от вашей позиции в этом более общем вопросе и ваших конкретных решений о типах. Если вы выберете более точное представление представления AST после синтаксического анализа, вы решите, включать ли уникальные идентификаторы или нет (я хотел бы, потому что генерация уникального идентификатора очень проста и не требует отдельного прохода, и я бы скорее немного усложнит грамматические произведения, чем этап набора текста). Если вы решите взломать поле типа с помощью фиктивного значения, также разумно сделать это для переменных идентификаторов, если вы хотите, указав 0 в качестве фиктивного значения и определив его позже; но все же я лично сделал бы это на этапе анализа.

...