Własny język skryptowy

0

Witam,
Po kilku próbach pisania języka skryptowego wreszcie chcę to zrobić porządnie. Z analizą tekstu z pliku nie będę miał problemu, także o to nie będę się martwił. Chodzi mi o to jak realizować sam język, tak aby było on dość wartościowy i można było go osadzić w programie jako narzędzie rozszerzające go, wydaje mi się, że w takim układzie interpretowanie linia po linii odpada. Jak wy byście to zrealizowali? Proszę o wszelkie redy, przepisy na napisanie tego oraz nawet najdrobniejsze wskazówki jak z taki przedsięwzięciem ruszyć.

Z góry dziękuję.
Pozdrawiam

0

A moze wez kod istniejacego jezyka np. Lua i zobacz jak to jest zrobione.

1

Chcesz to zrobic tak sobie zeby zrobic (a) czy naprawde pozniej produkcyjnie uzywac (b)?
Jesli (b) to nie ma najmniejszego sensu, jest juz pelno takich jezykow, wez ktorys z nich. Najpopularniejsze chyba to Groovy, JRuby oraz Clojure - chociaz wszystkie sa pelnoprawnymi jezykami standalone.
Jesli (a), to w sumie performance jest bez znaczenia ;d Ale, wiekszosc jezykow generuje bytecode (Groovy, Clojure, itp.). JRuby ma 2 lub wiecej sposobow dzialania - kompilacja do bytecodu lub fully-interpreted mode. Nie wiem ile umiesz, ale sadzac po tym ze tutaj pytasz o takie rzeczy sklania mnie do opinii ze raczej generacja bytecodu jest dla Ciebie poki co za trudna.
Co do analizy tekstu nie bedzie problemu - jak chcesz to zrobic, z ciekawosci pytam? Czy ten jezyk ma byc kompletny w sensie turinga, czy do jakichs specyficznych zastosowan? Sam napiszesz parser, czy skorzystasz z jakiegos generatora z BNF? Pomyslales formie przejsciowej aby dokonywac optymalizacji? Itp. itd.

0

Chodzi mi tu o zastosowanie domowe może w jakiejś aplikacji na androidzie potem. Generalnie bardziej interesuje mnie wartość poznawcza takiego zagadnienia, jestem studentem informatyki trochę już programuję i chcę się rozwijać a myślę, że to będzie niezła gimnastyka.
Jeśli mówisz o generowaniu bytecodu w sensie takiego jak opisany jest tu na podstawie analizowanego tekstu to jak dla mnie nie widzę większego problemu. Problem widzę w tym jak to potem wykonać i w zasadzie to może być moje kolejne pytanie w tym temacie :)
No tak można to zrobić operując na stosie ale jakoś słabo to do mnie przemawia :P
Inaczej bo zaraz wyjdzie, że nie wiem co to jest stos, powiedzmy, że generuję sobie taki bytecode i mam już go pięknie na stosie w pamięci teraz kolejno zdejmuję ze stosu instrukcje. Powiedzmy, że mamy taką sytuację:
Deklaruję sobie funkcję w skrypcie np taką:

def foo(x){
    x = x * 10
    print(x)
}

Generuje mi się tam jakiś bytecode

teraz gdzieś niżej wywołuję tą funkcję z parametrem np 5, teraz pytanie jak ona się wykona skoro bytecode definicji tej fcji zastał już zdjęty ze stosu?
To w zasadzie jedyne czego w tej całej instytucji języka skryptowego nie jest dla mnie zrozumiałe.
Ewentualnie może źle to rozumiem, więc proszę o wytłumaczenie :)

Co do analizy cięcie na słowa po delimiterach, może wyrażenia regularne, bo analiza po znaku nie wydaje mi się zbyt dobra chyba, że się mylę, jeśli tak to proszę o uświadomienie w błędzie.

czy skorzystasz z jakiegos generatora z BNF? Pomyslales formie przejsciowej aby dokonywac optymalizacji? Itp. itd.

Co do tego to aż tak w temacie się nie zagłębiałem chciałbym napisać coś co działa i ma jakąś wartość użytkową :)

0

scala - dsl
drools - dsl

zacznij od tego.

0

No wlasnie, tak jak myslalem - masz baaardzo powierzchowne pojecie na temat niezbednych przy tworzeniu wlasnego jezyka zagadnien, i do tego nie masz zadnego pomyslu co chcesz robic.
Nie do konca czaje o co ci chodzi ze stosem. Jak wygenerujesz bytecode Javy (bo jestesmy w dziale JVM) to mozesz wczytac klase ktora sie kryje za bytecodem (klasa to najmniejsza jednostka w JVM) i ja zdefiniowac i po prostu wykonac - zainteresuje sie ClassLoaderem. Ale zanim ty wygenerujesz poprawny kod ktory JVM wczyta to minie sporo czasu, i nie mam tu na mysli ze jestes slaby czy cos, tylko to jest trudniejsze niz ci sie w tej chwili wydaje. Ale powodzenia.
Co do analizy tekstu - split po delimiterach? WTF? Tak chcesz zrobic wlasny jezyk? Regexy - juz lepiej, ale szybko uderzysz w sciane. Wlasna gramatyka i generator parserow lub parser z palca - tak sie to robi.

0

IMO nie ma co zabierać się za tworzenie czegoś całkowicie nowego jeżeli nie poznało się fundamentów, w oparciu o które nowy twór ma powstawać.

0

teraz gdzieś niżej wywołuję tą funkcję z parametrem np 5, teraz pytanie jak ona się wykona skoro bytecode definicji tej fcji zastał już zdjęty ze stosu?

Bytecode tej funkcji mógłby wyglądać np.tak (tutaj na podstawie mojego języka (patrz: stopka)):

foo:
{1} mul([-1], 10) // [-1] -> argument ze stosu oddalony o "1" element od aktualnego wskaźnika stosu; [0] to adres powrotu, a po nim występują kolejne argumenty funkcji; konwencja wywoływania cdecl
{2] push([-1])
{3] call(:print)
{4} sub(stp, 1) // stp - wskaźnik stosu
{5] ret()

Moja VM-ka jest połączeniem stosowej z rejestrową (a nie czysto stosowa, jak np. JVM), lecz i tak powinieneś zrozumieć ideę:

  1. mnożymy x przez 10 (po prostu mnożąc przekazany do naszej funkcji argument 10)
  2. wrzucamy x na stos, by móc wywołać funkcję print (przypominam: konwencja wywoływania to cdecl)
  3. wywołujemy funkcję print
  4. czyścimy po niej stos
  5. wracamy do miejsca wywołania naszej funkcji foo (w działaniu identyczne jak assemblerowe ret)

Aby wywołać tę naszą funkcję, należałoby najpierw przekazać jej argument na stos, wywołać ją, a potem wyczyścić po niej stos, np.:

push(10) // wrzucamy liczbę całkowitą "10" na stos
call(:foo)
sub(stp, 1) // usuwamy liczbę "10" ze stosu

push(4) // wrzucamy liczbę całkowitą "4" na stos
call(:foo)
sub(stp, 1) // usuwamy liczbę "4" ze stosu

Wyświetliłoby po kolei liczby 100 oraz 40 (10*10=100, 4*10=40).

Wydaje mi się, że nie do końca rozumiesz ideę bytecode-u/opcodów - może najpierw zainteresuj się jakimiś kursami Assemblera (abyś poznał, jak to działa w przypadku x86 czy co tam sobie wybierzesz - wystarczą jakieś same podstawy), a dopiero potem zacznij myśleć o tworzeniu własnego języka, ponieważ o ile coś w stylu Brainfuck-like jest banalne do napisania (kiedyś nawet takie coś napisałem na rzecz konkursu na tym forum, zaledwie kilkaset linijek kodu afair), o tyle pisanie języka, który posiada (bardziej) zaawansowaną składnię (nie mówiąc o kompilowaniu do bajtkodu) jest czymś, co już wymaga pewnej wiedzy.

Co do analizy cięcie na słowa po delimiterach, może wyrażenia regularne, bo analiza po znaku nie wydaje mi się zbyt dobra chyba, że się mylę, jeśli tak to proszę o uświadomienie w błędzie.

Może przedstaw nam dokładniej, jak miałoby wyglądać to cięcie po separatorach lub wyrażenia regularne, bo jestem ciekaw, jak chciałbyś opisać w ten sposób składnię swojego języka...
Tak swoją drogą, wszystko i tak sprowadza się do analizy znak po znaku.

Co do tego to aż tak w temacie się nie zagłębiałem chciałbym napisać coś co działa i ma jakąś wartość użytkową

To nie jest "zagłębianie się aż tak", tylko podstawy...
Zresztą, przeczysz samemu sobie:

nie chciałem się aż tak zagłębiać w to
chciałbym napisać coś, co ma jakąś wartość użytkową

0

Co do analizy cięcie na słowa po delimiterach, może wyrażenia regularne, bo analiza po znaku nie wydaje mi się zbyt dobra chyba, że się mylę, jeśli tak to proszę o uświadomienie w błędzie.

Daleka droga przed tobą ;) A język to miałbyś mega ubogi gdyby był językiem regularnym (innego nie sparsujesz regexpami). Doczytaj sobie co to są języki bezkontekstowe, parsery LL, LR, SLR i LALR. Rzuć okiem na generatory lexerów/parserów -> Flex, Bison, YACC, AntLR.

0
Gjorni napisał(a):

IMO nie ma co zabierać się za tworzenie czegoś całkowicie nowego jeżeli nie poznało się fundamentów, w oparciu o które nowy twór ma powstawać.

Akurat wydaje mi sie ze wlasnie ma sens, bo te fundamenty po drodze, krok po kroku, sie pozna. Nie ma sensu jednak porywac sie na tworzenie czegos co ma niby produkcyjnie dzialac...

0

Co do parsera to jeszcze nie robiłem ale co do VM to powiedzcie mi czy coś takiego ma rację bytu:

package org.miziak.mscript.vm;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

import org.miziak.mscript.instructions.Add;
import org.miziak.mscript.instructions.Halt;
import org.miziak.mscript.instructions.Instructions;
import org.miziak.mscript.instructions.Int;
import org.miziak.mscript.instructions.Load;
import org.miziak.mscript.instructions.Nop;
import org.miziak.mscript.instructions.Out;
import org.miziak.mscript.instructions.Pop;
import org.miziak.mscript.instructions.Push;
import org.miziak.mscript.instructions.Str;
import org.miziak.mscript.instructions.Sub;
import org.miziak.mscript.instructions.Var;

public class Machine {
	private boolean running = false;
	private Instructions[] memory;
	public int adress = 0;
	public Stack<Integer> stack = new Stack<Integer>();
	public Map<String, Integer> vars = new HashMap<String, Integer>();
	
	public Machine(int mem_size) {
		memory = new Instructions[]{
			new Nop(this),
			new Nop(this),
			new Nop(this),
			new Push(this),
			new Int(this, 22),
			new Push(this),
			new Int(this, 11),
			new Add(this),
			new Var(this),
			new Str(this, "x"),
			new Nop(this),
			new Load(this),
			new Str(this, "x"),
			new Push(this),
			new Int(this, 3),
			new Sub(this),
			new Out(this),
			new Halt(this)
		};
	}
	
	public void run(){
		running = true;
		
		while(running){
			memory[adress].run();
		}
		
		System.out.println(stack);
		System.out.println(vars);
	}
	
	public void halt(){
		running = false;
	}
	
	public Instructions getMemoryCell(int adress) {
		return (Instructions)memory[adress];
	}
	
	public void setMemoryCell(Instructions s) {
		memory[adress] = s;
	}
}
0

Jak na początek to nie jest źle (chociaż nie rozumiem, co miałyby robić opcody var, push oraz out, biorąc pod uwagę, że np.push nie przyjmuje żadnych parametrów), natomiast musisz jeszcze wymyślić sposób, w jaki zapiszesz ten bajtkod do pliku.
I właśnie tutaj zasadniczą wadą Twojej VM-ki jest to, że będziesz musiał mieć mechanizm, który przy wyczytywaniu bajtkodu przetłumaczy go na tablicę Instructions[], podczas gdy przeważnie takie rzeczy są wykonywane w czasie rzeczywistym, np.:

Var Memory: Puint8;

Function NextByte: uint8;
Begin
 Result := Memory^;
 Inc(Memory);
End;

Function NextString: String;
Var Char: uint8;
Begin
 Result := '';
 
 While (true) Do
 Begin
  Char := NextByte;
  if (Char = 0) Then
   Exit;

  Result += chr(Char);
 End;
End;

Procedure _push_string;
Begin
 Stack.PushString(NextString);
End;

Procedure _write_string;
Begin
 Write(Stack.PopString);
End;

Procedure ParseOpcode;
Const OpcodeTable: Array[0..1] of Procedure = (@_push_string, @_write_string);
Var Opcode: uint8;
Begin
 Opcode := NextByte; // wczytujemy z pamięci jeden bajt wskazujący na typ opcodu
 OpcodeTable[Opcode](); // zależnie od opcodu czyta on z pamięci następne bajty będące jego parametrami
End;

Procedure Run;
Begin
 While (true) Do
  ParseOpcode;
End;

W ten sposób wykonana VM-ka może uruchomić przykładowo taki kod:
\0 H e l l o \0 \1
Pierwsze \0 oznacza, że opcode to push_string, bezpośrednio po nim występuje parametr (ciąg znaków zakończony terminatorem (null-character-em)), a na końcu mamy write_string, czyli VM-ka wyświetliła by w konsoli ciąg Hello; oczywiście brakuje tutaj jeszcze opcodu stop (lub halt), ale to taki szczegół ;P
Ta VM-ka parsuje i wykonuje kod w tej samej chwili, podczas gdy u Ciebie musiałbyś najpierw wszystko przeparsować, zapisać do tablicy Instructions[] i dopiero stamtąd wykonywać, co mogłoby być znacznie wolniejszym rozwiązaniem, niż to przedstawione powyżej (chociaż przeparsowywanie opcodów bywa czasami przydatne, aby wykryć potencjalnie szkodliwy kod (o ile się nie mylę, to JVM tak robi)).

0

Znaczy tak, nie wiem może źle to wymyśliłem ale push wydaje rozkaz przejścia do kolejnej "komórki pamięci" pobrania z niej liczby i wrzucenia na stos. Var miało mi implementować zmienne nie miałem na nie pomysłu i są wrzucane do hashmapy. Out robi pop na stosie i wyświetla wartość.
Wiem, że te Var jest do bani ale nie miałem innego pomysłu.
Co do zapisu bytecodu do pliku myślałem nad dodaniem do interfejsu metody w stylu toString po czym "przeleceniu" tablicy z bytecodem i kolejnym zapisie tego co zwraca toString.
Co do formy to myślałem o podejściu assemblerowym czyli

push 5
push 6
add
out
0
Miziak napisał(a)

Wiem, że te Var jest do bani ale nie miałem innego pomysłu.

A od czego masz stos?

Co do zapisu bytecodu do pliku myślałem nad do interfejsu dodaniu metody w stylu toString po czym przeleceniu tablicy z bytecodem i kolejnym zapisie tego co zwraca toString.

VM-ka zapisująca bajtkod, tego jeszcze nie było...

0

Znaczy nie dokładnie o to mi chodziło gdy już jakiś Parser/kompilator czy jak zwał to tak zwał stworzy w pamięci tą tablicę to zostanie ona zapisana tak jak to napisałem, a dopiero przekazana do VM.
Chociaż zastanawiam się teraz czy ma to jakiś większy sens :P

1 użytkowników online, w tym zalogowanych: 0, gości: 1