type terminal = string type non_terminal = string type symbol = | Terminal of terminal | NonTerminal of non_terminal type production = symbol list type rule = non_terminal * production type grammar = { start : non_terminal; rules : rule list; }No que se segue, admitiremos que todos os não terminais são acessíveis e produtivos. Admitiremos igualmente que o símbolo de partida S' está associado a uma única regra da forma S' -> S# onde # é um símbolo terminal que não aparece em mais nenhuma regra da gramática. Assim, o exemplo exposto nas aulas teóricas
E -> T E' E' -> + T E' | epsilon T -> F T' T' -> * F T' | epsilon F -> ( E ) | intfica codificado pelo valor OCaml :
let g_arith = { start = "S'"; rules = [ "S'", [ NonTerminal "E"; Terminal "#" ]; "E", [ NonTerminal "T"; NonTerminal "E'"; ]; "E'", [ Terminal "+"; NonTerminal "T"; NonTerminal "E'"; ]; "E'", [ ]; "T", [ NonTerminal "F"; NonTerminal "T'"; ]; "T'", [ Terminal "*"; NonTerminal "F"; NonTerminal "T'"; ]; "T'", [ ]; "F", [ Terminal "("; NonTerminal "E"; Terminal ")"; ]; "F", [ Terminal "int" ]; ] }
val fixpoint : ('a -> 'a * bool) -> 'a -> 'atal que fixpoint f x itera a função f partindo do valor x enquanto o booleano devolvido por f valer true.
module Ntset = Set.Make(String) type nulls = Ntset.t
Definir uma função is_null_production que, dado o conjunto dos não-terminais que podem derivar a palavra vazia, determina se uma palavra pode derivar-se na palavra vazia.
val is_null_production : nulls -> symbol list -> bool(É a função NULL(α) dada na aula.)
Deduzir a definição de uma função null : grammar -> nulls que calcula o conjunto dos não-terminais de uma dada gramática. Poderá utilizar-se a função fixpoint da forma seguinte :
let null g = let step nulls = ...calculamos o novo conjunto de nulls e indica-se por um booleano se houve mudanças... in fixpoint step Ntset.emptyNota : é, claro, possível utilizar Ntset.equal para determinar se o conjunto nulls mudou, mas é igualmente possível ser mais preciso usando is_null_production somente sobre as regras cujo membro esquerdo ainda não está em nulls e detectando neste caso se nulls precisa ser modificado.
module Ntmap = Map.Make(String) module Tset = Set.Make(String)O conjuntos dos primeiros que vamos agora calcular nesta questão terão consequentemente os tipos seguintes :
type firsts = Tset.t Ntmap.tou seja, un dicionário que associa a cada símbolo não-terminal um conjunto de símbolos terminais.
Definir uma função val empty_map : grammar -> Tset.t Ntmap.t que constrói um dicionário que associa a cada não-terminalda gramática um conjunto vazio de terminais.
Definir uma função
val first_production_step : nulls -> firsts -> symbol list -> Tset.tque calcula o conjunto dos primeiros de uma produção, dados o conjunto dos não-terminais nulos e um dicionário para os primeiros de cada não-terminal. (É a função FIRST(α) dada na aula.)
Deduzir do esforço prévio uma função val first : grammar -> nulls -> firsts que calcula o conjunto dos primeiros dos não-terminais de uma gramática, dado o conjunto dos não terminais nulos desta gramática (este é passado em parâmetro para evitar ser recalculado). Para este efeito, poder-se-á usar a função fixpoint e determinar se o conjunto dos primeiros é modificado, utilizando a função Tset.subset que calcula o predicado de conjuntos.
type follows = Tset.t Ntmap.t
Definir uma função
val follow : grammar -> nulls -> firsts -> followsque calcula os seguintes de uma gramática, dados os seus não-terminais nulos e os seus primeiros. Poderemos adotar o esquema seguinte :
let follow g nulls firsts = let update (follows,b) nt follow_nt = ... ... actualizar a tabela follows com nt -> follow_nt ... e substituir b por true se a tabela foi alterada ... in let rec update_prod ((follows,b) as acc) nt p = ... ... examinar a produção nt -> p da gramática ... e actualizar o par (follows,b) para todo o não-terminal X de p ... in let step follows = List.fold_left (fun acc (nt,p) -> update_prod acc nt p) (follows,false) g.rules in fixpoint step (empty_map g)
module Tmap = Map.Make(String) module Pset = Set.Make(struct type t = production let compare = compare end)Define-se então o tipo seguinte para as tabelas de análises descendentes :
type expansion_table = Pset.t Tmap.t Ntmap.tNuma tabela destas é um dicionário associando a cada não-terminal e também a cada terminal um conjunto de produções. Os dois dicionários são ocos : quando uma linha ou uma coluna da tabela encontra-se vazia, não há entrada correspondente na tabela.
Definir uma função
val add_entry : expansion_table -> non_terminal -> terminal -> production -> expansion_tableque junta uma entrada na tabela. Dever-se-á ter em atenção o correcto tratamento do caso em que uma primeira ocorrência de uma linha ou de uma coluna.
Definir uma função
val expansions : grammar -> expansion_tableque calcula uma tabela de análise descendente de uma dada gramática.
O código produzido poderá ser testado com a gramática seguinte (que caracteriza as palavras que contêm tantos a quantos b) :
let g1 = { start = "S'"; rules = ["S'", [NonTerminal "S"; Terminal "#"]; "S", []; "S", [Terminal "a"; NonTerminal "A"; NonTerminal "S"]; "S", [Terminal "b"; NonTerminal "B"; NonTerminal "S"]; "A", [Terminal "a"; NonTerminal "A"; NonTerminal "A"]; "A", [Terminal "b"]; "B", [Terminal "b"; NonTerminal "B"; NonTerminal "B"]; "B", [Terminal "a"]; ] } let table1 = expansions g1Para visualizar o resultado, poderemos utilizar o código seguinte que define um
let pp_symbol fmt = function | Terminal s -> Format.fprintf fmt "\"%s\"" s | NonTerminal s -> Format.fprintf fmt "%s" s let rec pp_production fmt = function | [] -> () | [x] -> pp_symbol fmt x | x :: l -> Format.fprintf fmt "%a %a" pp_symbol x pp_production l let pp_table fmt t = let print_entry c p = Format.fprintf fmt " %s: @[%a@]@\n" c pp_production p in let print_row nt m = Format.fprintf fmt "@[Expansions for %s:@\n" nt; Tmap.iter (fun c rs -> Pset.iter (print_entry c) rs) m; Format.fprintf fmt "@]" in Ntmap.iter print_row tUsando o exemplo acima introduzido, o resultado de
let () = Format.printf "%a@." pp_table table1deve ser o seguinte :
Expansions for A: a: "a" A A b: "b" Expansions for B: a: "a" b: "b" B B Expansions for S: #: a: "a" A S b: "b" B S Expansions for S': #: S "#" a: S "#" b: S "#"Testar igualmente com a gramática g_arith dada no início deste enunciado.
let table_arith = expansions g_arith let () = Format.printf "%a@." pp_table table_arith
Testar com
let () = assert (is_ll1 table1) let () = assert (is_ll1 table_arith)
Testar igualmente com uma gramática da sua escolha que não seja LL(1).
val analyze : non_terminal -> expansion_table -> string list -> boolque determina se uma palavra é reconhecida por uma gramática, dados o não-terminal inicial e a sua tabela de expansão. (Não nos preocuparemos aqui em saber se a tabela corresponde a uma gramática LL(1) e, em caso de ambiguidade escolhemos uma produção ao acaso com recurso à função Pset.choose.) Não esquecer juntar o símbolo "#" no fim da palavra.
Poderemos testar com a gramática g1 utilizando o código seguinte
let explode s = let n = String.length s in let rec make i = if i = n then [] else String.make 1 s.[i] :: make (i+1) in make 0 let test1 s = analyze g1.start (expansions g1) (explode s) let () = assert (test1 "") let () = assert (test1 "ab") let () = assert (test1 "ba") let () = assert (test1 "abab") let () = assert (test1 "aaabbb") let () = assert (test1 "aaabababbbababab") let () = assert (not (test1 "a")) let () = assert (not (test1 "b")) let () = assert (not (test1 "aab")) let () = assert (not (test1 "aaabbba")) let () = assert (not (test1 "aaabbbaabab")) let () = assert (not (test1 "aaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb"))
let g_gram = { start = "S'"; rules = [ "S'", [ NonTerminal "S"; Terminal "#" ]; "S", [ NonTerminal "R" ]; "S", [ NonTerminal "R"; Terminal ";"; NonTerminal "S" ]; "R", [ Terminal "ident"; Terminal "::="; NonTerminal "P"]; "P", [ NonTerminal "W" ]; "P", [ NonTerminal "W"; Terminal "|"; NonTerminal "P" ]; "W", [ ]; "W", [ NonTerminal "C"; NonTerminal "W";]; "C", [ Terminal "ident"]; "C", [ Terminal "string"]; ] }Esta é a gramática das gramáticas, em que os diferentes não-terminais significam :
Verificar que esta gramática não é LL(1) :
let table_gram = expansions g_gram let () = Format.printf "%a@." pp_table table_gram let () = assert (not (is_ll1 table_gram))
Propor uma gramática LL(1) que reconhece esta linguagem.