Изградете интерпретатор в Java - Внедрете механизма за изпълнение

Предишна 1 2 3 Страница 2 Следваща Страница 2 от 3

Други аспекти: Струни и масиви

Две други части на езика BASIC се изпълняват от интерпретатора COCOA: низове и масиви. Нека първо разгледаме изпълнението на низове.

За да се приложи низове като променливи, Expressionкласът е модифициран, за да включва понятието за изрази "низ". Тази модификация е под формата на две допълнения: isStringи stringValue. Източникът на тези два нови метода е показан по-долу.

String stringValue (Program pgm) изхвърля BASICRuntimeError {хвърля нов BASICRuntimeError ("Няма представяне на String за това."); } boolean isString () {return false; }

Ясно е, че не е твърде полезно за BASIC програма да получи низовата стойност на базов израз (който винаги е или числов, или булев израз). От липсата на полезност може да заключите, че тогава тези методи не принадлежат Expressionи Expressionвместо това принадлежат към подклас . Чрез поставянето на тези два метода в базовия клас, всички Expressionобекти могат да бъдат тествани, за да се види дали всъщност те са низове.

Друг подход за проектиране е да се върнат числовите стойности като низове, като се използва StringBufferобект за генериране на стойност. Така например, същият код може да бъде пренаписан като:

String stringValue (Program pgm) хвърля BASICRuntimeError {StringBuffer sb = new StringBuffer (); sb.append (this.value (pgm)); върнете sb.toString (); }

И ако се използва горният код, можете да елиминирате използването на, isStringзащото всеки израз може да върне стойност на низ. Освен това можете да модифицирате valueметода, за да се опитате да върнете число, ако изразът се изчислява на низ, като го стартирате чрез valueOfметода на java.lang.Double. В много езици като Perl, TCL и REXX този вид аморфно писане се използва с голяма полза. И двата подхода са валидни и вие трябва да направите своя избор въз основа на дизайна на вашия преводач. В BASIC интерпретаторът трябва да върне грешка, когато низ е присвоен на числова променлива, затова избрах първия подход (връщане на грешка).

Що се отнася до масивите, има различни начини, по които можете да проектирате своя език, за да ги интерпретирате. C използва квадратните скоби около елементите на масива, за да разграничи индексните препратки на масива от референтните функции, които имат скоби около своите аргументи. Въпреки това, езиковите дизайнери за BASIC избраха да използват скоби както за функции, така и за масиви, така че когато текстът NAME(V1, V2)се вижда от анализатора, това може да бъде или извикване на функция, или референция на масив.

Лексикалният анализатор прави разлика между символи, които са последвани от скоби, като първо приема, че са функции и тества за това. След това продължава, за да види дали са ключови думи или променливи. Именно това решение пречи на вашата програма да дефинира променлива с име „SIN“. Всяка променлива, чието име съвпада с име на функция, ще бъде върната от лексикалния анализатор като функция маркер вместо това. Вторият трик, който използва лексикалният анализатор, е да провери дали името на променливата е непосредствено последвано от "(". Ако е, анализаторът приема, че е референтен масив. Като анализираме това в лексикалния анализатор, ние премахваме низа " MYARRAY ( 2 )'от интерпретиране като валиден масив (обърнете внимание на интервала между името на променливата и отворената скоба).

Последният трик за внедряване на масиви е в Variableкласа. Този клас се използва за екземпляр на променлива и както обсъдих в колоната от миналия месец, той е подклас на Token. Той обаче разполага и с някои машини за поддържане на масиви и това е, което ще покажа по-долу:

class Variable extends Token {// Legal type sub types final static int NUMBER = 0; окончателен статичен int STRING = 1; окончателен статичен int NUMBER_ARRAY = 2; окончателен статичен int STRING_ARRAY = 4; Име на низ; int подтип; / * * Ако променливата е в таблицата със символи, тези стойности са * инициализирани. * / int ndx []; // масивни индекси. int mult []; // множители на масиви удвояване nArrayValues ​​[]; String sArrayValues ​​[];

Горният код показва променливите на екземпляра, свързани с променлива, както в ConstantExpressionкласа. Човек трябва да направи избор относно броя на класовете, които ще се използват спрямо сложността на даден клас. Изборът на дизайн може да бъде да се изгради Variableклас, който съдържа само скаларни променливи и след това да се добави ArrayVariableподклас за справяне с тънкостите на масивите. Избрах да ги комбинирам, превръщайки скаларните променливи по същество в масиви с дължина 1.

Ако прочетете горния код, ще видите индекси на масиви и множители. Те са тук, защото многомерните масиви в BASIC се изпълняват с помощта на един линеен Java масив. Линейният индекс в Java масива се изчислява ръчно с помощта на елементите на множителния масив. Индексите, използвани в програмата BASIC, се проверяват за валидност, като се сравняват с максималния правен индекс в ndx масива на индексите .

Например, BASIC масив с три измерения 10, 10 и 8, ще има стойностите 10, 10 и 8, съхранявани в ndx. Това позволява на анализатора на изрази да тества за условие „индекс извън границите“, като сравнява използвания в програмата BASIC номер с максималния правен номер, който сега се съхранява в ndx. Множителният масив в нашия пример ще съдържа стойностите 1, 10 и 100. Тези константи представляват числата, които човек използва, за да преобразува от многомерна спецификация на индекса на масив в спецификация на индекс на линеен масив. Действителното уравнение е:

Индекс на Java = Index1 + Index2 * Максимален размер на Index1 + Index3 * (MaxSize of Index1 * MaxSizeIndex 2)

Следващият масив Java в Variableкласа е показан по-долу.

 Израз expns []; 

В expns масива се използва за да се справят с масиви, които са написани като " A(10*B, i)". В този случай индексите всъщност са изрази, а не константи, така че препратката трябва да съдържа указатели към тези изрази, които се оценяват по време на изпълнение. И накрая има този доста грозен на вид код, който изчислява индекса в зависимост от това какво е предадено в програмата. Този частен метод е показан по-долу.

private int computeIndex (int ii []) изхвърля BASICRuntimeError {int offset = 0; if ((ndx == null) || (ii.length! = ndx.length)) хвърли нов BASICRuntimeError ("Грешен брой индекси."); за (int i = 0; i <ndx.length; i ++) {if ((ii [i] ndx [i])) хвърля нов BASICRuntimeError ("Индекс извън обхвата."); отместване = отместване + (ii [i] -1) * mult [i]; } връщане на отместване; }

Разглеждайки кода по-горе, ще забележите, че кодът първо проверява дали правилен брой индекси са били използвани при препращане към масива и след това дали всеки индекс е в рамките на законовия диапазон за този индекс. Ако се открие грешка, на интерпретатора се изпраща изключение. Методите numValueи stringValueвръщат стойност от променливата съответно като число или низ. Тези два метода са показани по-долу.

double numValue (int ii []) хвърля BASICRuntimeError {return nArrayValues ​​[computeIndex (ii)]; } String stringValue (int ii []) изхвърля BASICRuntimeError {if (subType == NUMBER_ARRAY) return "" + nArrayValues ​​[computeIndex (ii)]; връща sArrayValues ​​[computeIndex (ii)]; }

Има допълнителни методи за задаване на стойността на променлива, които не са показани тук.

Като скрива голяма част от сложността на това как се изпълнява всяка част, когато накрая дойде време за изпълнение на програмата BASIC, кодът на Java е съвсем ясен.

Изпълнение на кода

Кодът за интерпретиране на инструкциите BASIC и тяхното изпълнение се съдържа в

run

метод на

Program

клас. Кодът за този метод е показан по-долу и ще премина през него, за да посоча интересните части.

1 публичен празен ход (InputStream in, OutputStream out) изхвърля BASICRuntimeError {2 PrintStream pout; 3 Изброяване e = stmts.elements (); 4 stmtStack = нов стек (); // приемаме, че няма подредени изявления ... 5 dataStore = new Vector (); // ... и няма данни за четене. 6 dataPtr = 0; 7 Изявление; 8 9 варианта = ново RedBlackTree (); 10 11 // ако програмата все още не е валидна. 12 if (! E.hasMoreElements ()) 13 return; 14 15 if (out instanceof PrintStream) {16 pout = (PrintStream) out; 17} else {18 pout = new PrintStream (out); 19}

Горният код показва, че runметодът приема InputStreamи OutputStreamза използване като "конзола" за изпълняващата програма. В ред 3 обектът на изброяване e е зададен на набор от изрази от колекцията, наречена stmts . За тази колекция използвах вариант на двоично дърво за търсене, наречено "червено-черно" дърво. (За допълнителна информация относно бинарни дървета за търсене вижте предишната ми колона за изграждане на общи колекции.) След това се създават две допълнителни колекции - една с помощта на Stackи друга с помощта наVector. Стекът се използва като стека във всеки компютър, но векторът се използва изрично за операторите DATA в програмата BASIC. Окончателната колекция е друго червено-черно дърво, което съдържа референциите за променливите, дефинирани от програмата BASIC. Това дърво е таблицата със символи, която се използва от програмата, докато се изпълнява.

След инициализацията се настройват входните и изходните потоци и след това, ако e не е null, започваме със събирането на всички декларирани данни. Това се прави, както е показано в следващия код.

/ * Първо зареждаме всички изявления за данни * / while (e.hasMoreElements ()) {s = (Statement) e.nextElement (); if (s.keyword == Statement.DATA) {s.execute (this, in, pout); }}

Горният цикъл просто разглежда всички изрази и всички DATA изрази, които намери, се изпълняват. Изпълнението на всеки оператор DATA вмъква декларираните от него изрази във вектора dataStore . След това изпълняваме правилно програмата, което се прави с помощта на следващата част от кода:

e = stmts.elements (); s = (Изявление) e.nextElement (); направи {int yyy; / * По време на изпълнение пропускаме изявленията за данни. * / опитайте {yyy = in.available (); } catch (IOException ez) {yyy = 0; } if (гггг! = 0) {pout.println ("Спрян на:" + s); тласък (и); почивка; } if (s.keyword! = Statement.DATA) {if (traceState) {s.trace (this, (traceFile! = null)? traceFile: pout); } s = s.execute (this, in, pout); } else s = nextStatement (s); } докато (s! = нула); }

Както можете да видите в горния код, първата стъпка е да се реинициализира e . Следващата стъпка е да извлечете първия оператор в променливата s и след това да въведете цикъла за изпълнение. Има някакъв код, който да се провери за изчакващо въвеждане във входния поток, за да се позволи прекъсването на напредъка на програмата чрез въвеждане в програмата и след това цикълът проверява дали изразът за изпълнение ще бъде DATA израз. Ако е, цикълът пропуска изявлението, тъй като вече е изпълнено. Необходима е доста обърканата техника за първо изпълнение на всички оператори с данни, тъй като BASIC позволява операторите DATA, които отговарят на оператор READ, да се появят навсякъде в изходния код. И накрая, ако е разрешено проследяването, се отпечатва запис на проследяване и твърде невзрачното изявлениеs = s.execute(this, in, pout);се извиква. Красотата е, че всички усилия за капсулиране на основните понятия в лесни за разбиране класове правят крайния код тривиален. Ако не е тривиално, тогава може би имате представа, че може да има друг начин да разделите дизайна си.

Опаковане и допълнителни мисли

Интерпретаторът е проектиран така, че да може да работи като нишка, като по този начин може да има няколко нишки на интерпретатора COCOA, работещи едновременно във вашето програмно пространство. Освен това, с помощта на разширяване на функциите, ние можем да осигурим средство, чрез което тези нишки могат да взаимодействат помежду си. Имаше програма за Apple II, а по-късно и за PC и Unix, наречена C-robots, която представляваше система от взаимодействащи „роботизирани“ обекти, програмирани с помощта на прост език на производни BASIC. Играта осигури на мен и другите много часове забавление, но беше и отличен начин да запозная основните принципи на изчисленията с по-малките ученици (които погрешно вярваха, че просто играят, а не учат).Базираните на Java интерпретаторски подсистеми са много по-мощни от техните аналози преди Java, защото те са незабавно достъпни на всяка платформа на Java. COCOA работи на системи Unix и Macintoshes същия ден, когато работих на компютър, базиран на Windows 95. Докато Java се бие от несъвместимости в изпълненията на инструментите за нишки или прозорци, това, което често се пренебрегва, е следното: Много код „просто работи“.