Konstruktion av datorspråk Fö4: Domänspecifika språk och parsning Peter Dalenius petda@ida.liu.se Institutionen för datavetenskap Linköpings universitet 2009-02-12 Domänspecifika språk Ett domänspecifikt språk (DSL) är ett oftast litet språk vars syfte är att uttrycka problem eller lösningssätt för ett specifikt begränsat problemområde. Externt DSL: Ny syntax som inte är gemensam med något existerande programspråk. Kräver helt nya verktyg, men man kan skapa ett uttrycksfullt språk för det specifika problemet. Internt DSL: Utvidgning av ett existerande programspråk (en typ av abstraktion), med fördelen att man lätt kan bearbeta koden. Några fördelar och nackdelar Exempel Mer uttryckskraft inom en specifik domän gör det lättare att uttrycka problem. (+) Möjlighet för icke-programmerare att specificera problem. (+) Extra arbete att producera verktyg för externt DSL. (-)
Var går gränsen för DSL? Olika språkgenerationer Varning för hype! Möjlig motsats: allmänt general-purpose programspråk (t.ex. C++, Java, Ruby) Möjligt överlapp: scriptspråk knutna till och avsedda att styra en viss applikation, men är ofta mer lika riktiga programspråk Möjligt överlapp: fjärde generationens språk (se nästa bild) 1GL: Maskinspråk 2GL: Assembler 3GL: Moderna språk, från 1960-talet och framåt (t.ex. Fortran, ALGOL, Pascal, C, C++, Java, Ruby) 4GL: Applikationsspecifika språk 5GL:??? Exempel på eget DSL Verktyg för eget DSL article 1234 name "Sune" description "Book shelf" color "Red" inventory 42 article 5678 inventory 29 name "Berra" color "Brown" description "Mirror" Denna textfil innehåller information om ett antal artiklar i ett lager. Betrakta textfilen som programkod och försök skriva ett verktyg som kan tolka den. Hur då? class Inventory < Array def Inventory.load(filename) inv = new inv.instance_eval(file.read(filename)) inv def article(number) self << {:article => number} def method_missing(name,args) self.last[name] = args
Tekniker Kernel#load Kernel#eval Object#instance_eval Module#class_eval Kernel#method_missing Module#const_missing Exempel 2 (programkod) class Configuration def initialize(filename="config.rb") instance_eval(file.new(filename).read()) def method_missing(method_name,arg) @@var = method_name class << self class_eval { attr_accessor(@@var) } instance_eval("self.#{method_name}=#{arg}") Exempel 2 (körning) zaza1 <1> cat config.rb a 45 b 4711 c 1396 zaza1 <2> irb --simple-prompt >> load "configuration.rb" => true >> foo=configuration.new => #<Configuration:0x2fdda0c @c=1396, @b=4711, @a=45> >> foo.a => 45 Exempel 3 (Mofo) class HCalar < Microformat container :vevent one :class, :description, :dt, :dtstamp, :dtstart, :duration, :status, :summary, :uid, :last_modified, :url => :url, :location => [ HCard, Adr, Geo, String ] many :category class HCard < Microformat container :vcard one :fn, :bday, :tz, :sort_string, :uid, :class, :geo => Geo many :label, :sound, :title, :role, :key, :mailer, :rev, :nickname, :category, :note, :logo => :url, :url => :url, :photo => :url, :adr => Adr one :n do one :family_name, :given_name, :additional_name many :honorific_prefix, :honorific_suffix...
Grammatik expr ::= term term + term term term term ::= factor factor * factor factor / factor factor ::= number identifier ( expr ) 1 + ( 2 * 3 ) Analys av källkod 1. Lexikalisk analys Bildar tokens utifrån källkodens text. 2. Syntaktisk analys (parsning) Kontrollerar att koden är syntaktiskt korrekt Bygger upp en datastruktur (oftast en trädstruktur) som motsvarar källkoden för vidare bearbetning i nästa steg. 3. Semantisk analys Kontrollerar t.ex. typfel. 4. Fler steg, beroe på uppgiften Ström av tecken d e f f ( n ) \n n = = 0 - - - Verktyg för generering av verktyg Lexer def f ( n ) n == 0 - - - name f Parser function n param body - - - Ström av tokens AST (Abstrakt syntaxträd) Klassiska Unix-verktyg för C: lex är ett verktyg för att skapa lexers, där man specificerar språkets lexikaliska struktur med reguljära uttryck. yacc är ett verktyg för att skapa parsers, där man specificerar språkets syntax med en kontextfri grammatik (gärna i BNF). Det finns motsvarande verktyg för Ruby också, t.ex. ruby-lex och racc.
Parsern som vi använder i labben Användning av DiceRoller Två basklasser: Rule representerar en syntaktisk regel. Parser representerar själva parsern (generisk). Med hjälp av dessa skapar vi ett litet domänspecifikt språk så att vi lätt kan göra en egen parser för ett valfritt språk. I labbfilerna finns exempelparsern DiceRoller som är ett litet språk för att slå tärningar och räkna ut enkla matematiska uttryck. irb(main):1696:0> DiceRoller.new.roll [diceroller] 1+3 => 4 [diceroller] 1+d4 => 2 [diceroller] 1+d4 => 3 [diceroller] (2+8*d20)*3d6 => 306 Slå en tärning med fyra sidor och lägg till ett. Slå en tärning med tjugo sidor, multiplicera med åtta och lägg till två. Multiplicera detta med summan av tre tärningar med sex sidor. Alla tre klasserna Rule, Parser och DiceRoller finns i filen rdparse.rb. class DiceRoller def initialize @diceparser = Parser.new("dice roller") do token(/\s+/) token(/\d+/) { m m.to_i } token(/./) { m m } start :expr do match(:expr, '+', :term) { a, _, b a + b } match(:expr, '-', :term) { a, _, b a - b } match(:term) rule :term do match(:term, '*', :dice) { a, _, b a * b } match(:term, '/', :dice) { a, _, b a / b } match(:dice) rule :dice do match(:atom, 'd', :sides) { a, _, b DiceRoller.roll(a, b) } match('d', :sides) { _, b DiceRoller.roll(1, b) } match(:atom) rule :sides do match('%') { 100 } match(:atom) rule :atom do match(integer) match('(', :expr, ')') { _, a, _ a }