Code snippets, ideas and events from IT related projects

by Robert Gawron

Domain Specific Language in Ruby used as a Virtual Machine

22.01.2010 17:59, Posted in , 17 comments, 0 pingbacks

Abstract

Post describes a simple DSL language (in Ruby), that is used as a virtual machine, for a simple assembly language. This assembly language was used by me a couple of months ago, when I created a simple programming language - there are already implementation of its VM written in C (by czarodziej) and in Python (by me).

I was confused was what language should I use as a base for DSL. It should be flexible, expressive and with good support for metaprogramming. It couldn't be Java or PHP for sure! ;-) I considered Lisp, Ruby, Erlang and Python. My choice was Ruby.

Some people might say that it's not DSL at all, that it's in place where DSL shouldn't be used or that all is wrong. I didn't worry about those questions and treat it as an interesting experiment, but for sure it could be made better.

It isn't useless theory, I've seen those languages in my previous jobs (used to creating specialized tests scenarios or gathering in formations). The way of accessing data through Django's ORM is also sort of DSL.

Details of implementation

Part of assembler's syntax is invalid in Ruby, so I made small preprocessor, that runs before code is loaded and makes it valid. It's second purpose is to modify this code to the form that is more expressive in Ruby. It's coded in DSLPreprocesor class.

This is how assembly file looks before preprocesing:
bar:
    int 0
ret

foo:
    call bar
    ret

main:
    push 13
    call foo
    ret
and this is how it looks after, I've added indents to make code more readable:
@bar = Proc.new{
    int 0
}

@foo = Proc.new{
    call @bar
}

@main = Proc.new{
    push 13
    call @foo
}

We have to implement in Ruby all mnemonics, like call, add in form, that makes state and behavior of running virtual machine exactly like in original version. Then we have to evaluate processed file (there are only variables, so nothing will happened), and then run @main object.

You might be wondering why I didn't use normal functions instead of Ruby's mods, well, I needed to tread functions as objects (see implementation of call in DSLVirtualMachine).

You can obtain more information by analyzing source code (see bellow) or by confrontation with original version, you can also check project's website.

   1  #!/usr/bin/env ruby 
   2  
   3  class DSLVirtualMachine
   4      def initialize(file_name = String.new)
   5          @preprocesor = DSLPreprocesor.new file_name
   6          @stack = Array.new
   7          @memmory = Array.new
   8          @stack_ptr = [0]
   9          @memmory_ptr = [0]
  10      end
  11  
  12      def run()
  13          eval @preprocesor.parse()     # dynamic code loading
  14          @main.call()                  # code executing
  15      end
  16  
  17      private
  18  
  19      def alu(operation)
  20          parser = { 'add' => lambda { @stack.pop + @stack.pop },
  21                     'sub' => lambda { @stack.pop - @stack.pop },
  22                     'div' => lambda { @stack.pop / @stack.pop },
  23                     'mul' => lambda { @stack.pop * @stack.pop },
  24                     'gt'  => lambda { @stack.pop > @stack.pop }, }
  25          push parser[operation].call()
  26      end
  27  
  28      def call(function)
  29          @memmory_ptr.push @memmory.length
  30          function.call()
  31          @memmory_ptr.pop
  32      end
  33  
  34      def push(element)
  35          @stack.push element
  36      end
  37  
  38      def load(offset)
  39          memmory_ptr = @memmory_ptr.last
  40          @stack.push @memmory[offset+memmory_ptr]
  41      end
  42  
  43      def store(offset)
  44          memmory_ptr = @memmory_ptr.last
  45          @memmory[offset+memmory_ptr] = @stack.pop
  46      end
  47  
  48      def get_if_condition()
  49          return @stack.pop
  50      end
  51  
  52      def int(interrupt)
  53          parser = [ lambda { p @stack.pop },
  54                     lambda { push gets.to_i } ]
  55          parser[interrupt].call()
  56      end
  57  end
  58  
  59  
  60  class DSLPreprocesor
  61      attr_accessor :fname
  62  
  63      def initialize(fname = String.new)
  64          @fname = fname
  65          # input token => output token, order does matter
  66          @parser = {
  67              # remove comments
  68              /^([^;]*);.*$/  => 
  69                          lambda { |u| "#{u}" },
  70              # convert jump to label into 'if' statement
  71              # it's very LAME!
  72              /^[\s]*jz ([a-z0-9]*)$/ => 
  73                          lambda { |u| "if (get_if_condition)" },
  74              /^a[0-9]:*$/ => 
  75                          lambda { |u| "end" },
  76  
  77              # convert function label to declaration 
  78              # of proc in ruby ruby
  79              /^([a-z]*[0-9]*):$/  => 
  80                          lambda { |u| "@#{u} = Proc.new{" },
  81              # convert end of function in assembler 
  82              # to end of proc in in ruby 
  83              /^[\s]*ret$/  => 
  84                          lambda { |u| "}" },
  85              # 
  86              /^[\s]*call ([a-z0-9]*)$/  =>
  87                          lambda { |u| "call @#{u}" },
  88  
  89              # treat ALU operation in consistent way
  90              /^[\s]*(add|sub|mul|div|gt|lt|eq)$/ => 
  91                          lambda { |u| "alu '#{u}'" },
  92          }
  93      end
  94  
  95      def parse()
  96          file = File.readlines(self.fname)
  97          return file.map { |l| parse_mnemonic(l) }.join('')
  98      end
  99  
 100      private
 101      def parse_mnemonic(mnemonic)
 102          @parser.map { |k,v| mnemonic.gsub!(k){ v.call($1) } }
 103          return mnemonic
 104      end
 105  end
 106  
 107  # TODO use optparse instead of raw ARGV[0]
 108  vm = DSLVirtualMachine.new ARGV[0]
 109  vm.run()
 110  

Results

My implementation is incomplete and in some places incompatible with original, but I reached the point and build it. It becomes overcomplicated due to fact that syntax wasn't created to be compatible with Ruby's syntax. Second problem was my lack of experience with Ruby - to be honest I don't know this language at all.

It would be interesting to count SLOC, but I didn't have tool to do that (I don't have space on HDD to install it) and implementation in Ruby is incomplete. I count all lines in source files, for C implementation it was ~600, for Python ~250, for DSL+Ruby ~110.

Case studies and materials about DSL

Pingbacks

No pingbacks yet

Comments

avatar
czarodziej , 26.01.2010 7:45, reply

Wpierw pytanie dotyczące natury bloga, komentarze moge pisac po polsku? :)

Moj kiepski angielski wprowadza mnie w zaklopotanie, napisales na poczatku “as described above.”, ale ja musze przegladarka przewijac w dol ;)

Skladni ruby kompletnie nie rozumiem, ale eksperyment wydaje mi sie pozyteczny — upchales to do 110 lini, dobry wynik. Z drugiej strony to wolalbym zeby l33tlang sie w koncu kompilowal ;) Wiesz ze wedlug (chyba) wskazowek GNU wygenerowany kod za pomoca yacc powinno sie dolaczac do paczki z kodem, a nie meczyc uzytkownika z generowaniem tego?

I jeszcze tak od siebie dorzuce moje przemyslenia na temat tworzenia jezyka programowania. Po pierwsze haslo: jezyk powinno sie nie tworzyc przez dodawanie nowych bajerow, ale przez odrzucanie tego co powoduje ze nowe bajery wydaja sie potrzebne ;) Ja zaczalbym tworzenie jezyka od postawienia sobie celow jakie on musi spelniac. Dla mnie to jest: 1. Możliwosc uzywania jezyka jako wbudowany/skryptowy. (Ten punkt nie pozwala mi na stosowanie yacc itp) 2. Jezyk jak najprostszy z tym ze wyrazenia przekazywane jako parametry funkcji nie maja byc ewaluowane, dopiero jak funkcja tego sobie zazyczy podlegaja ewaluacji (chyba haskell to ma), wtedy nawet instrukcje takie jak IF moga byc funkcjami. 3. Domkniecia 4. Przyjazny sposob pisania wyrazen. Nie: “(* 2 (+ 2 2))”, tak: “2+2*2”.

avatar
Robert , 26.01.2010 13:36, reply

Cześć,

co do języka komentarzy, to oczywiście może to być polski, możne też być angielski i francuski (ici on parle francais:))), a jak będą w innych językach to nie będę usuwał.

Spotkałem się z tym, iż komentarze pisane po polsku na anglojęzycznych blogach prowadzonych przez Polaków są usuwane — IMHO to niegrzeczne wobec czytelników i jakieś takie infantylne.

Zastanawiałem się, czy zaczynać pisać po angielsku, bo kaleczę ten język, mimo to uznałem, że się to opłaca, gdyż więcej osób będzie mogło go czytać. Kilka razy pisząc z obcokrajowcami, spotkałem się z info, że kod, rysunki wyglądają ciekawie, no ale tekst jest po polsku więc nijak nie wiadomo o czym jest. Dodatkowo niedługo będę się rozglądał za pracą, pewnie gdzieś za granicą i umiejętność wysławiania się w języku pogan muszę ćwiczyć.

Odnośnie ‘above’ to masz rację, zmieniłem szyk akapitów, a nie zaktualizowałem treści, dzięki, poprawione! Dobra dość, o językach ludzi, przejdźmy do języków programistów :)

Co do ilości linii kodu, jak pisałem, w Ruby będzie tego więcej bo nie jest on skończony. Wszytko wyglądało by lepiej, gdyby projektując składnie asemblera myśleć o składni Ruby, w obecnej sytuacji to trochę na siłę jest to zrobione.

Co do generowania kodu z yacc’a to o tym nie wiedziałem, sądziłem, że jeżeli programista chce ściągnąć projektu by go modyfikować, to w to wlicza się modyfikacja plików yacc’a więc ma go zainstalowanego w systemie. Teraz zastanawiam się nad tym.. w sumie racja, ktoś może ściągać projekt by go skompilować i zainstalować — sam często tak robię. Jak będę miał czas/wenę to dodam pliki wynikowe yacc’a do repozytorium. Dzięki za info!

Odnośnie przyjaznego sposobu pisania wyrażeń arytmetycznych to ten, który zaproponowałeś IMHO jest mniej przyjazny od notacji polskiej, gdyż a) programista musi pamiętać, o kolejności wykonywania wyrażeń (komplikuje to też kompilator) b) czasem trzeba dodawać nawiasy, by wymusić kolejność.

Swoją drogą, projektując (o ile można nazwać to projektowaniem) l33tlang poszedłem dalej i nie użyłem +, -, *, tylko ich słowne odpowiedniki, wyjaśnię przy okazji dlaczego tak zrobiłem, bo wcale nie dlatego, by uprościć kompilator :)

Pokaże to na przykładzie Javy: jest metoda realizująca skomplikowane operacje arytmetyczne, nagle okazuje się, że musi działać na liczbach o znacznie większej precyzji albo o większym zakresie. Taką klasę realizującą operacje na dużych/precyzyjnych liczbach można napisać ale wtedy już zapisane operacje trzeba przepisać z formy

a + b

na coś w stylu:

a.add(b)
MyBigNumber.add(a, b)

bo w Javie nie można przeciążać operatorów, kod trzeba przeorać. Uniknąć tego można implementując w języku przeciążanie operatorów lub cały czas pisać tak, jak w drugim przykładzie, traktować operacje matematyczne, porównania jak zwyczajne funkcje (które są od razu zaimplementowane ale prócz tego niczym się nie różnią). Nie chciałem przeciążania operatorów więc wybrałem drugą metodę. Może jest jakaś inna droga? Swoją drogą w l33tlangu dalej nie można przeciążać żadnych funkcji więc to martwa funkcjonalność. EDIT czytam to teraz i sam nie mogę się połapać, w tym, co napisałem, anyway chodziło mi o to, by traktować te funkcje jak każdą inną, więc o zwykłej nazwie i bez dodatkowych opcji.

Kolejny problem, z wbudowywaniem funkcji widać np w Pythonie (nie jestem pewien, lecz chyba w najnowszej wersji już jest to poprawione), otóż ‘print’ jest wbudowany w język, więc nie da się go przeciążyć, np tak by po odpaleniu:

print 22

otrzymalibyśmy na ekranie 22 poprzedzone nazwą usera, z którego konta działa skrypt — tak się nie da, bo to nie jest zwykła funkcja. AFAIK temu też nie można jej używać w lambdach (chociaż nie jestem pewien). Nie chce mi się sprawdzać ale w PHP jest print i echo, jedna z nich jest zwykłą funkcją, druga słowem kluczowym języka, więc nie można jej przeciążyć.

Odnośnie Twojego pierwszego pkt, to jest bardzo ciekawa technika zapisu liczb: http://en.wikipedia.org/wiki/Church_encoding próbowałem się tego nauczyć i wpleść to wpleść w język ale odpuściłem.

Piszesz, dużo o tym, iż l33tlang’a nie można odpalić na urządzeniach osadzonych — to ciekawy temat, tylko, że ciężko się za niego zabrać — trzeba mieć jakieś urządzenie na którym się go będzie odpalało. OK, są emulatory ale fajnie byłoby od czasu do czasu widzieć, jak to działa :)

Myślałem głównie, czy dało by się go odpalić na jakimś routerze albo komórce? Na tym pierwszym nie znam się zupełnie ale tam śmigają jakieś *nixy + można tam coś wgrać. Nie wiem, poszukam info w wolnym czasie. Co do komórek — moja nie ma Javy ;(

Fajnie byłoby też odpalić go na mikroprocesorze, tylko trzeba mieć programator, mój laptop nie ma portu szeregowego/równoległego a by zbudować programator na USB AFAIK trzeba już mieć mikroprocesor do jego (tzn programatora) zbudowania. Kolejną barierą jest też to, że większość softu i manuali jest pod windowsa.

Kompilacja do pliku wykonywalnego również jest bardzo ciekawym zagadnieniem.

Poruszyłeś sporo innych kwestii w komentarzu, postaram się na nie odpisać później.

pozdrawiam

avatar
czarodziej , 26.01.2010 17:00, reply

hej, pragne przedstawic Ci moje powody dla ktorych wole zwykla notacje od polskiej. Tak jak wspomnialem milo by bylo zeby argumenty funkcji nie musialy byc koniecznie ewaluowane przy jej wywolaniu, a to implikuje funkcje eval() w jezyku, ta z kolei moze byc przydatna dla koncowego uzytkownika, wyobrazmy sobie program rysujacy wykres funkcji, prosi uzytkownika o podanie wyrazenia i… uzytkownik ma uczyc sie notacji jaka zastosowalem przy budowie jezyku? A moze do takiego programu pisac parser wyrazen, albo po prostu skorzystac z notacji jakiej nas od przedszkola ucza :)

Oczywiscie Twoja uwaga o naglej checi zmiany typu na BigNum jest trafna, ale zaraz java ma operatory ktorych nie mozna przeciazac, tak wiec jak mi sie zachce zmieniac typ to z operatora i tak musze przejsc na funkcje… I tu sie pojawia fundamentalny problem, o ktorym oboje zapomnielismy. Dla kogo tworzony jest jezyk, czy robimy jezyk z “ubezpieczeniem”, tj. np jak jest w l33tlangu ze + to funkcja po to by programista mogl sobie ja potem przeciazyc, czy uznajemy ze programista wie najlepiej i jak bedzie chcial w przyszlosci przeciazac to zaprojektuje program na funkcjach, a jak nie to uzyje operatorow. Ja uwazam ze kazdemu nalezy sie szacunek ;)

Co do slow kluczowych, ja bym sie ich calkiem pozbyl, ograniczyl sie tylko do lambda (chyba ze wymysle jak lambda moze byc funkcja). Oj juz wiem, skoro wywolanie funkcji nie ewaluluje parametrow to lambda moze byc funkcja o nieokreslonej liczbie parametrow gdzie kolejne parametry to wyrazenia ktore maja byc w tworzonej funkcji. ;)

Ah, i jeszcze musze dodac cos odnoscie tego nieszczesnego yacc, tak naprawde to nie moge miec pretensji o to bo pobieralem kod z svn, ale jak bedziesz robil paczke to tak samo jak z np. configure.ac, w paczce sie tego nie zalacza tylko to co autoconf wygeneruje z tego, podobnie mysle o yacc.

avatar
czarodziej , 26.01.2010 17:03, reply

Znowu zegarek zle chodzi, ja to pisze o godzinie 18.03

avatar
Robert , 26.01.2010 18:05, reply

Nom, godziny przy komentarzach to plaga tego bloga, już chyba 3-4 raz jest nie tak:) Pewnie w pliku konfiguracyjnym trzeba ustawić strefę czasową albo kraj, później zobaczę ale thx za uwagę.

avatar
Robert , 26.01.2010 18:26, reply

Cześć,

odnośnie przykładu z programikiem, to IMHO jest to zbyt rzadka sytuacja by na jej podstawie zmieniać kolejność argumentów wyrażeń arytmetycznych (która IMHO taka jaka jest ma swoje plusy).

Wyjść z takiej sytuacji IMHO było by dość sporo, parser, DSL, można by osadzić parser innego języka (python, lua) z zwyczajnym zapisem i z niego pobrać dane. Swoją drogą, programik rysujący funkcje i udostępniający userowi jakieś API (czy jak to nazwać) to też jakaś forma DSL.

Tak jak wspomniałem milo by było żeby argumenty funkcji nie musiały być koniecznie ewaluowane przy jej wywołaniu, a to implikuje funkcje eval() w języku”

Niekoniecznie, jeżeli funkcja podawana jest bez nawiasów na końcu może być traktowana jako obiekt, jeśli z nawiasami, to jako funkcja do wywołania, np. takie coś:

def foo(a b opr) {
opr(a b)
}

add(1 3) # wywoluje funkcje add (wbudowana implementacja)
foo(1 2 add) # wywoluje funkckje foo, zwrca 3

Implementacja l33tlanga na razie jest bardzo skąpa, polimofizm, domknięcia i inne rzeczy to póki co odległe sprawy. Kiedyś zbierałem informację, jak działa taki język programowania zacząłem wgryzać się też w polimorfizmy, temat na razie zbyt obcy mi by cokolwiek obmyślić sensownego.

ograniczyl sie tylko do lambda (chyba ze wymysle jak lambda moze byc funkcja). Oj juz wiem, skoro wywolanie funkcji nie ewaluluje parametrow to lambda moze byc funkcja o nieokreslonej liczbie parametrow gdzie kolejne parametry to wyrazenia ktore maja byc w tworzonej funkcji” — pogmatwane, muszę to przemyśleć.

A tak w ogóle to chyba przeniosę repozytorium projektu na jakiś serwis hostujący projekty w rozproszonym systemie kontroli wersji. Często mam wenę, jak jestem offline, no a wtedy nie mogę commitować niczego, muszę się posiłkować robieniem na boku kopii plików, no a później taki wielki commit wysyłać do repozytorium.

Czytałem na blogu google.code, że mają umożliwić dostęp przez rozproszony system, no ale jakoś to ucichło. Szkoda :(

pozdrawiam & dzięki za uwagi

avatar
Robert , 26.01.2010 18:28, reply

..a tak w ogóle to nie drażni Cię, że piszę ‘część’? w sumie gadaliśmy trochę na gg no i w komentarzach na tym blogu ale nadal nie wiem ile masz lat :)

avatar
czarodziej , 26.01.2010 19:07, reply

mam 22 lata i jednak to nie znacznik <code> tak ladnie dzialal w Twoich postach :)

avatar
Robert , 27.01.2010 12:04, reply

No to jestem starszy o 3 lata =) A kod wcina się jednym tabem albo czterema spacjami.

avatar
czarodziej , 26.01.2010 19:04, reply

Cześć :)

Ja jak najbardziej rozumiem ze funkcja z nawiasami to wywolanie a bez to obiekt, tak jest w C :)

Mi chodzi o takie rzeczy:

defun funkcja (liczba a, wyrazenie b) { ... }

wywołanie:
funkcja(5 + 2, 2 + 2 * 2)

Przy wywołaniu 5 + 2 jest ewaluowane i do funkcji przekazywana jest liczba 7, nastepny parametr nie jest ewaluowany (bo funkcja spodziewa sie wyrazenia) i jak jest potrzeba to sobie je ewaluluje (robi eval(b) ). Dlatego napisalem ze to implikuje funkcje eval(). Jakze fajny bylby jezyk :

defun if(bool warunek, wyrazenie jesli_tak, wyrazenie jesli_nie) {
return eval(warunek ? jesli_tak : jesli_nie)
}

i od teraz mamy if'a:

if (2 + 2 < 6, say("foo"), say("bar")
; pierwszy argument jest ewaluowany i jest przekazywana prawda/fałsz a kolejne sa przekazywane tak jak byly napisane.

Pseudokod powyzej ma powazny blad logiczny, wiem, to tak zeby zaprezentowac idee.

Wybacz ze tak z lambda zagmatwalem. Wyobraz sobie taka lambde:

funkcja = lambda(tablica typu wyrazenie lista_parametrow, wyrazenie lista_wyrazen, ...)
;zwraca funkcje pobierajaca liste_parametrow i wykonujaca wyrazenia.

np.
add = lambda((wyrazenie []){liczba a, liczba b}, a + b)

add(3, 5) => 8

Eh, i tak mam wrazenie ze niewiele pomogl ten przyklad. A może to ja calkiem nie Cie nie zrozumialem?

Jak juz tak pisze o funkcjach w funkcjach to wiedziales ze w C mozna deklarowac funkcje tak:

int foo(int bar(int, int), int x) { return bar(x, 7L); }

;)

avatar
Robert , 27.01.2010 12:01, reply

Cześć,

to, co opisałeś w pierwszym przykładzie można prosto osiągnąć w Haskellu:

my_if con stmt1 stmt2  = if con/=0 then stmt1 else stmt2

main = do my_if 12 (print "czesc") (print "papa")
my_if 0 (print "czesc") (print "papa")

w Pythonie można by to symulować np. tak:

def my_if(cond, stmt1, stmt2):
if cond:
return stmt1()
return stmt2()

def say(u):
print u

my_if(12, lambda: say('czesc'), lambda: say('papa'))
my_if(0, lambda: say('czesc'), lambda: say('papa'))

Dobrze Cię zrozumiałem? IMHO masz rację, takie działanie jest bardzo dobre, a w wolnym czasie poprawię to w l33tlangu. Będę też musiał jakiś manual czy specyfikacje napisać :) Thx za uwagę!

Co do drugiego przykładu to nie rozumiem, do czego wykorzystywana jest tablica ‘wyrazenie’?

Co do kodu w C — jest straszny =]

avatar
czarodziej , 27.01.2010 15:53, reply

Tak, wlasnie o takie zachowanie jak w haskellu mi chodzi. Calkiem sprytna jest tez symulacja tego w pythonie jaka mi pokazales, z tym ze konieczne jest wywolywanie lambdy, to troche glupipie bo jesli piszemy wlasnego if’a to chodzi o to zeby ewaluowane bylo tylko to co if ma “wykonac”.

W moich przykladach typ “wyrazenie” nie podlegalby ewaluacji przy wywolaniu funkcji, kazdy inny typ (np “liczba”) bylby ewaluowany.

Tablica wyrazenie byla tam po to bo zakladajac ze tworzenie funkcji to tylko wywolanie lambda(), jakos nalezy odroznic parametry tworzonej funkcji od jej ciala — parametry do tablicy a cialo jako reszta parametrow lambdy.

Może ja za nadto komplikuje, ale uwzgledniajac te wymysly mozna zrobic jezyk ktory ma tylko jedna regule: wszystko jest wyrazeniem, tj ciagiem zmiennych lub wywolan funkcji polaczonch operatorami. Zadnych slow kluczowych, zadnych cukrow skladniowych, zadnych wyjatkow w skladni.

avatar
Robert , 28.01.2010 19:43, reply

Cześć,

IMHO zamiast składni:

add = lambda((wyrazenie []){liczba a, liczba b}, a + b)

lepsze byłoby (parametry funkcji odróżnione są przez {}):

add = lambda({liczba a, liczba b}, a + b)

o ile dobrze roumiem, to co w klamerce, mówi, że a i b są typu liczna, to w języku dynamicznie typizowanym było by niepotrzebne, tzn starczyło by samo {a,b}.

Podoba mi się ta ostatnia skłania =)

Przemyślę to na spokojnie (czyli jak będę offline) i za jakiś czas napiszę notkę, jak bym składnie l33tlanga widział, jeśli wywiąże się dyskusja (mam nadzieję:-)), to uwzględnieniu zmian napiszę specyfikację. Składnia asemblera IMHO jest całkiem przyzwoita, poza dodaniem wsparcia dla stringów nic jej nie brakuje.

Fajnie było by też znaleźć coś innego niż yacc+lex.

Na razie kod kompilujący z l33tlanga do asma jest strasznie chaotyczny i zjebany.

Swoją drogą, zacząłem pisać kompilator, tworzący binarki pod Linuksa, na razie wygląda to na proste zajęcie ale wymagające wgłębienia się w specyfikacje. Kompiluje plik asemblera do pliku ELF, a później linkuję go programem ld. Miałem trochę dylematów technicznych, jak ktos by chciał poczytać: http://groups.google.com/group/pl.comp.lang.asm/browse_thread/thread/6370e0b070106935#

Rozmawiałem też z kolegą (pozdro Krzysiek B. jeśli kiedykolwiek to przeczytasz :-)), jak wgląda odpalanie binarek na konsolkach. Napisanie kompilatora i odpalenie tego języka na czymś takim było by super! AFAIK nie jest to obiecująca droga.

pozdrawiam

avatar
Robert , 1.02.2010 19:23, reply

Cześć,

Jeszcze odnośnie przeciążania operatorów, to czasem wygląda ono zupełnie nielogicznie, tzn wiedząc, jak działa klasa nijak nie można się domyślić, jak działa taki przeciążany operator, przykładowo (to będzie kontrowersyjny przykład:-)):

#include <iostream>

int main() {
std::cout << 4;

int foo = 5;
std::cin >> foo;
}

No i widząc obiekt klasy do obsługi wyświetlania napisów nijak można się domyślić, że przesuwając go o 4 bity w lewo wyświetlamy te czwórkę na ekranie, zaś w drugiem przypadku, iż przesuwając bitowo o 5 w prawo zapisujemy z STDIN do zmiennej, w której jest ta piątka.

OK, ten kod jest zrozumiały do kogoś, kto się C++ uczył ale to IMHO tylko dla tego, iż takiej obsługi std::cin, std::cout uczy się od początku.

Takie stosowanie przeciążania operatorów IMHO jest nielogiczne i zaciemnia kod.

IMHO przeciążanie jest OK dla bytów matematycznych (bo przeciąża się głównie operacje matematyczne, zależy od języka) jak tablice, wektory, liczby zespolone dla takich bytów powinna być jakaś opcja, by to umożliwić. Ale dla wszystkiego innego (a to wszystkie inne IMHO stanowi większość) nie.

IMHO język poprawnie implementujący przeciążanie operatorów powinien umożliwiać też zmianę ich pierwszeństwa.

Co do l33tlanga, nie podoba mi się jego składnia, IMHO jest zbyt mało ekspresywna, nie ma wsparcia dla lambd i funkcji wyższego rzędu, etc. Chciałbym coś prostego, ekspresywnego, nawet jeśli musiało by być trudniejsze w nauce. Pomyślałem o takiej składni:

require "package name"

mul : a b -> a*b
add : a b -> a+b

print add mul 3 3 mul 2 2

comment "comment here"

u : limit -> map print range 1 limit

To, co jest przed komentarzem to pierwszy przykład, a to, co po nim drugi, ten drugi tworzy obiekt u przyjmujący jeden argument n oraz wypisujący liczby od 1 do n.

IMHO było by też fajnie, gdyby można było wykorzystywać biblioteki napisane w C, zastanawiam się na razie jak.

Było by też fajnie, gdyby język miał budowę modułową, np tak:

  • ktoś pisze rozszerzenie języka korzystając z (jakiejś) nomenklatury, to rozszerzeni jest kompilowane do biblioteki, którą można załadować dynamicznie, do tego pisze dokumentację, jak się tej nowe funkcjonalności używa,
  • w katalogu projektu jest config, w którym jest lista dynamicznie ładowanych bibliotek, do tego katalog w którym te liby są,
  • podczas kompilacji czytany jest config, ładowane te liby i wykorzystywane procedury z nich
  • co by nie było w libach i jakie nowe rzeczy nie były by załadowane, kod jest kompilowany do asemblera a później wykonywany przez maszynę wirtualną lub kompilowany do kodu natywnego

To tylko szkic, jednak IMHO w miarę prosto możliwy do osiągnięcia i oferujący sporo możliwości (no i totalnie nieprzenośny). Obmyślając to chciałem, by język sam w sobie był materiałem, a nie zbiorem reguł.

Więcej rzeczy opiszę później. Jak to widzisz(widzicie)?

pozdrawiam

avatar
Robert , 1.02.2010 23:57, reply

BTW przeniosłem projekt do GitHub’a: http://github.com/RobertGawron/l33tlang

avatar
czarodziej , 2.02.2010 17:13, reply

Cześć, W pierwszych akapitach poruszyłeś kwestie przeciążania operatorów i w 100% to co napisałeś pokrywa się z moimi przemyśleniami na ten temat.

Co do prezentowanej składni, to chyba jej do końca nie ogarniam ;)

mul : a b -> a * b  /* to tworzy funkcje/operator który mnoży? ale przecież
już został użyty operator '*' -- rozumiem że takie operatory jak mnożenie muszą być wbudowane */

Rozważając nad składnią uważam że nad każdym pojedynczym elementem należy się zastanowić i rozważyć plusy i minusy. Oczywiście jakieś bardziej ogólne zasady przy jej projektowaniu tęż należy mieć, ja np proponowałbym takie dwa punkty:

  • składnia jest czytelna, dla programisty i dla maszyny; dlaczego? bo kod jest pisany dla programisty, ale warto też mieć eval_string() :)
  • nie ma dużej liczby cukrów składniowych; dlaczego? po co mi dwie wersje robienia czegoś samego, wole jedna ale porządną.

Właściwie to tak należałoby się wpierw zastanowić jaki/do czego ma być język. :)

Na koniec, nie rozumiem co czyniłoby opisaną przez Ciebie modułową budowę “total nie nieprzenośną”. (poza kompilowaniem do linux-elf ;))

avatar
Robert , 4.02.2010 11:33, reply

Cześć,

odnośnie przykładu, który podałem, to operatory arytmetyczne i logiczne były by takie, jak w starej wersji, czyli add, mul, and i inne, a jeśli ktoś chciałby to mógłby napisać rozszerzenie do języka i mieć te operatory ‘szkolne’ +, -, *. Przykład zagmatwałem ale miałem w tym swój cel ;-)

Część osób, które tym zaspamowałem zwróciła uwagę na kolejność wykonywania operacji (notacja polska, odwrotna notacja polska), np:

zamiast tego:
print add mul 3 3 mul 2 2

, żeby było coś takiego:
2 2 mul 3 3 mul add print

.. a może by lispowato?
(print (add (mull 2 2) (mul 3 3)))

Co do przenośności modułów, które byłyby dopisywane do języka, to jeśli były by one używane jako binarki to trzeba by był mieć dla każdego wspieranego OS’a binarkę, jeśli zaś były by dołączane jako kod źródłowy (i np. kompilowane przed odpaleniem kompilatora), to przenośność byłaby takim samym problemem, jak w zwykłych aplikacjach w C, czyli wszystko by zależało od tego, kto pisze i czy ktoś testuje na wspieranych platformach.

Myślałem, czy takie moduły pisać w Javie i (jakoś) to złączyć z kompilatorem l33tlanga w C (to też jest IMHO wdzięczne pole do eksperymentów) ale staram się unikać Javy, IMHO jest zbyt mało ekspresywna, więc to IMHO zły pomysł.

Pomyślałem też, iż można by pisać te moduły w l33tlangu i to na razie wydaje mi się najlepszym wyjściem.

BTW było by super, gdyby można było wykorzystywać biblioteki w C ‘ot tak’ :)

Co do języka, to chciałbym, by można go było wykorzystać do pisania skryptów administracyjnych, aplikacji desktopowych, softu na urządzenia osadzone.

nie ma dużej liczby cukrów składniowych; dlaczego? po co mi dwie wersje robienia czegoś samego, wole jedna ale porządną.

To IMHO zależy też, do tego, jakie/ile paradygmatów wspiera dany język, przykładowo, żeby wyświetlić wszystkie elementy tablicy w Pythonie można (imperatywny, funkcyjny):

def say(s):
print s

list = [2, 3, 4]

# pierwsze
for i in list:
say(i)

# drugie
filter(lambda i: say(i), list)

co do przeciążania, heh, rzadko się w czymś w całości zgadzamy :)

pozdrawiam

Leave your reply

Let me know what you think

Required. 30 chars of fewer.

Required.

captcha image