Advanced-Programing-FinalReport
- Question 1: APPY: A simple parser generator
- Question 2: Stock Exchange
- Appendix
Question 1: APPY: A simple parser generator
Question 1.1: The Parser module
Design
I first get rid of Left recursion and got the whole grammer as follows:
Spec ::= preamble ERules.
ERules ::= ERule | ERule ERules.
ERule ::= LHS "::=" Alts ".".
LHS ::= name OptType | "_".
OptType ::= | "{:" htext "}".
Alts ::= Seq | Seq "|" Alts.
Seq ::= Simple | Simplez "{" htext "}".
Simplez ::= | Simple Simplez.
Simple ::= Simple0 | Simple0 "?" | Simple0 "*" | "!" Simple0.
-- Simple0 ::= Atom Simple0'.
Simple0' ::= | "{?" htext "}" Simple0
Atom ::= name | tokLit | "@" | charLit | "(" Alts ")".
implement
I implemented the parsers from top to bottom, just follow the grammar and write them.
First I implemented skipmore to skip spaces and comments, this part of the code I referenced from the 2021 exam sample code. Then I made token, symbol and schar from Parsernotes, using skipmore in the token can make it skip spaces and comments before any token.
In parseLhs I used isUpper to check the head of the LHS name, if it's Upper then it's an RPlain, or it's an RToken. So I just wrote them from parseErules to parseAtom. Then I made 4 small parsers to parse names (read a letter first and read a lot of letters,digit, and underscores),tokit(begin with double-quotes and end with double-quotes,I didn't implement double double-quotes because I'm not quite sure how to judge if a double-quote is the ending of a TokLit or not),CharLit(begin with a single-quote and read a printable char and end with a single-quote),At(just return SAnyChar),parseHtext(read a lot of printable chars and end with '}', I didn't implement double '{'}' because I'm also not sure how to judge if it's an end or not).
Then, I implemented parsePreamble to read anything until it meets a '-', and read "---" and return what it just read.
While developing I found that some of my grammers may generate ambiguous results, so I used the last result in parseSpec. It's not the best way to handle it, but at least it can return the correct parsing answer.
Test
I made 34 unit tests for the parser, some of the results is failed because I didn't implyment those part, such as complicated htext and tokLit with double-quotes inside.But the rest are all performing well and following is the test result:
Question 1.2: The Transformer module
I managed to implement a version of convert which can convert the test sample eg into g, it uses patton matching to get the rhs, and then I check the type of it, if it's ESimple then I will return a list with AUser "". If it's a ESeq I'll return the list generated by the helper function removeEsimple with Auser which is the second parameter of ESeq.If it's a EBar, I first check the two parameters of EBar, if they are both ESeq I will return a list with two ESeq's convert result.This simple convert function can successfully convert the sample eg into g.
Question 2: Stock Exchange
Question 2.1: The erlst module
Topics
- I support the complete API
- My trader can only process one Strategy at a time, so it needs to be not so slow.
- I uses 2 processes, one is the main exchange, one is the Strategy spawned process. Main exchange uses gen_server APIs to receive messages from user, Strategy send function result to main process through "!"
- The data is shown below as the status
- These parts are discussed in implement part
Design
I read through the questions first and constructed the status of the system. As there's 3 main data sets we need to save, I added 3 lists representing Accounts,Offers and Traders.Each Account has an account ID and a holding.
In designing account ID I found there're some functions that don't have server Pid as input, but have just an Acct. So I think AccountID needs to contain it's server Pid, in that way it can get the server which this account was opened in, so the accountID is {S,Id}.For the Id part we need a unique id for each account, there's multiple ways to do it, including setting a user count in state or generate a random unique number each time. I used the second one because I don’t want to add extra data in status which may result in the server running slow. I used erlang:now() to get the current time. I used erlang:localtime() first but during test I figured out that the minimum unit of time is seconds and if I call it several times by sequence erlang will do it in a second, and their ID will be the same. So I used now( ) instead although it's been deprecated. I used lists: concat( )to concat ‘A' and 3 results meaning it's an accountID. Also I used the same code in generating OfferID and TraderID with initials of O and T.
In designing Offers as we need to store an accountID with the offer to do the money/ stock transfer later I added AccountID into the offer tuple. Also we need to add AccountID into Trader to do the following trades.
As we need to return the number of trades I also add a number into the status to count for it, I will discuss it later.
The following is the status I designed:
1 | State is [Num,Accounts, Offers, Traders] |
implement
I used gen_server for the whole system and reviewed my code from assignment 6 to recall how to use APIs in gen_server.
Open_account
It will receive the server and new account's holding and make a call to the server. Server will first generate an ID and use a helper function checkHoldings(Holdings).This function will check the input Holding if the ISK >= 0. Then it will call another function checkStocks(). This function will check if each of the amounts >=0. Then if checkHoldings return true we will reply it’s AcctID and also add this item into State.If it’s false we will return {error,badHoldings} to show it’s not a valid holding.
Make_offer
I used a helper function check_Account_exist to run account_balance with a given Id, and judge its result and return true or false. If it’s a valid Acct then we call the server with input Offer and Id from Acct. In server I first judge if the offer is a valid tuple, then if it’s valid we check if the offer’s price is valid.If all of these conditions are met we generate an OfferID and add the new offer into the state.
Add_trader
Then it comes the most complicated part of the server, the trader.
The task asked me to execute the trade whenever 4 conditions are met. I think we can define a function to execute each tradeable trade. As the 4 conditions may only be met when addoffer and addtrader is called, so I put the function above into handle_call of make_offer and add_trader.
My design of helper functions in doing this is as follows:
- check_offer_exists which will check if the offer list is empty.
- run_strategy: run a strategy on a single offer. In this function I spawned a new process and in this process it runs the strategy over offer and sends back the result after the function gets the result to run_strategy process. And in the main time run_strategy receives the result and returns it. In this part I didn’t implement
“If a trader has accepted and is able to execute a trade, the stock exchange should execute the trade without waiting for the decisions of other—possibly slow—traders.”
The reason of it is when I get the idea of doing it there's not enough time for me to change the whole structure, I think it maybe can be solved by using lists:foreach for each Strategy to spawn a new process and send back the result, and the main process receive the fastest result and return it's traderID and result.
- Map_offer: use one trider run al offer, return a list of results the Trader made to each of the offers,which may looks like this: [accept,reject,accept.....]. this list is called Map_Res.Here each result is a tuple, it's look like {reject,Buyer,{OfferId,Seller,{Stock,Price}}}.With these info we can proceed to handle changing accounts.
- Run_all_traders: use all traders to run all offer, each trader will return a list with result, so the function will return a list of result lists, which may looks like this:[[accept,reject],[reject,reject],[accept,accept]...],this list is called Map_Reses
- Remove_offer: it will receive Map_Res and Offers, remove those offers which is accept in Map_Res.
- Transfer_stock(Map_Res,Accounts): I used a helper function with 3 parameters in this part.As I remove offer based on Map_Res, sometimes Map_Res is accept, but the holdings of two side doesn't meet the requirement, so although the trade was not exec,the offer is also got removed. So this function will return new Map_Res and new Accounts.In helper function it has a new para NewMap_Res to collect each of new results.In this function it will go through each Map_Res, if it's accept it will check buyer's ISK and then seller's stock, then do the transfer or do nothing to the Accounts list.
- Run_all_Map_Res(Map_Reses,Accounts,Offers,Num):Recursive traversal Map_Reses and use above two functions to change Accounts and Offers. Also I changed the trade num by subtracting old Offers length with new Offers length and add the change to Original Num.Then it returns the final status after trading.
assessment
Completeness :I completed all the APIs
Correctness: I made 16 unit tests and 2 props and both passed(will discuss next)
Robustness: I made several robust upgrade to erlst after running eqc and unit test,including fixed a bug that change_stock may return a list with {error,aaa}inside, fixed several functions may crush when receiving a tuple.Before these my erlst crushes a lot when running prop test but now it doesn't crush any more.However,because of time I didn't implement the robust requirement, but I think both of them can be resolved with supervisor behavior.
Maintainability: I used a lot of helper functions in the code which makes it very easy to find bug and fix it. Also my code's neatly arranged and each section has it's name, like Trader helper functions,server API and so on.
2.2 Testing erlst
Topic
- I had implemented all required parts
Design
I implemented the function mkstrategy with 4 possible choices:{buy_everything,buy_cheap,buy_only,both}and each of them can return a strategy function.in both function it will only return accept when two functions generated by mkstrategy both return accept.
Then I implemented some other generators, stock_name can generate a name from[a,b,c,d,e,f], in stock_list,holdings and offer I used ?SUCHTHAT to check if the price > 0, this can make sure they can generate legal ISK and prices.
Then I implemented reliable_strategy generator. It used oneof to generate a symbolic call from four choices regarding to four choices of mkstrategy.
Then I implemented prop_value_preservation().This prop I need to check the whole amount of money and stock, I first added two accounts, and each of then make 4 offers and 4 traders.And then check the money and stock. I used two helper functions calc_Total_Money and calc_Total_Stock to get current whole money and stock.Then I implemented prop_total_trades(), it also gen 2 accounts and each make 4 offers and 4 traders.Then the number of make_offer calls is 8 and number of trades is the number shutdown returns.
I also added 16 unit tests to test some aspects prop may not be able to reach, like negative ISK, negative stock price or offer price and so on, and my erlst passed all the tests.
Following is the result of testing:
assessment
Completeness :I completed two props and some other helper functions
Correctness: I used the props to test my own erlst and found several bugs in my system until all tests passed.
Appendix
ParserImpl
1 | -- Put yor parser implementation in this file |
TransformerImpl
1 | -- Put yor transformer implementation in this file |
BlackBox.hs
1 | -- Sample black-box test suite. Feel free to adapt, or start from scratch. |
erlst.erl
1 | -module(erlst). |
Test_erlst_unit.erl
1 | -module(test_erlst_unit). |
Test_erlst.erl
1 | -module(test_erlst). |