Domain Specific Language in Ruby used as a Virtual Machine
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
- Ruby
- Lisp
- Erlang
- JavaScript
- Python
- Here is link, how to do that in Python, IMHO it's not as sexy as in Ruby, Erlang or others.
Pingbacks
No pingbacks yetComments
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
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.
Znowu zegarek zle chodzi, ja to pisze o godzinie 18.03
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ę.
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
..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 :)
mam 22 lata i jednak to nie znacznik <code> tak ladnie dzialal w Twoich postach :)
No to jestem starszy o 3 lata =) A kod wcina się jednym tabem albo czterema spacjami.
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); }
;)
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 =]
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.
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
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
BTW przeniosłem projekt do GitHub’a: http://github.com/RobertGawron/l33tlang
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 ;))
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
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”.