Recently i was working on a project that is processing campaigns. I asked my self if I can improve it a bit, but not by refactoring existing code but by trying to approach it from a different angle.
Observations
I started making mind notes of things that i understand about system i’m working with. I never had time to think about grammar of this and really get into it. The system has many parts but i thought it can be reproduced in for of language that is easy to parse and nice to work with. I think i might change my current solution into more LISP looking thing just to make it easier to parse.
Observation 1
Each campaign is a sequence of actions that happens one after another. So my initial thought was “this is simple”. We can represent it by list of actions like this
1
[step] -> [step] -> [step]
But this is true only for simple campaigns and most of them are NOT like this :(
Observation 2
Most of the campaigns are built around key thing. This thing is making decisions! So if i can incorporate “if” i WIN !
Lets have a look
Yes! This was it, i think this is solution to all problems, ability to represent campaign as a sequence of steps mixed with if statements essentially abstract syntax tree of campaign.
Observation 3
It is not a tree…its more a graph. But AST will make it :] it is a programming language! So this was in my head and i did not had time to work on it… but today i decided to give a try and I made first impression of AST that we would need to have to make it work.
AST
I wrote simple grammar and made a parser of a very simple “ify” language. My language starts like this…
123456789101112131415
-- Grammar
-- x is variable
-- a is artmetic thing aka number
-- n is actual number eg. 1
-- b is boolean
-- opa is arthmetic operator
-- opb is boolean operator
-- opr is comparison operator
-- S is statement
-- a ::= x | n | - a | a opa a
-- b ::=true | false | not b | b opb b | a opr a
-- opa ::= + | - | * | /
-- opb ::= and | or
-- opr ::= > | <
-- S ::= x | x <- a | S1; S2 | ( S ) | if b then S1 else S2
This shows that the grammar is very simple, we can have assigments, operators to test and compare and ofc IF statement ;) this is our key to divnity!
Parser looks kinda ugly because i used parts of the code i wrote before and had i in different projects.
importSystem.Environment(getArgs)importSystem.IOimportControl.MonadimportText.ParserCombinators.ParsecimportText.ParserCombinators.Parsec.ExprimportText.ParserCombinators.Parsec.LanguageimportqualifiedText.ParserCombinators.Parsec.TokenasToken-- Grammar-- x is variable-- a is artmetic thing aka number-- n is actual number eg. 1-- b is boolean-- opa is arthmetic operator-- opb is boolean operator-- opr is comparison operator-- S is statement-- a ::= x | n | - a | a opa a-- b ::= true | false | not b | b opb b | a opr a-- opa ::= + | - | * | /-- opb ::= and | or-- opr ::= > | <-- S ::= x | x <- a | S1; S2 | ( S ) | if b then S1 else S2 dataAExpr=VarString|IntConstInteger|NegAExpr|ABinaryABinOpAExprAExprderiving(Show)dataBExpr=BoolConstBool|NotBExpr|BBinaryBBinOpBExprBExpr|RBinaryRBinOpAExprAExprderiving(Show)dataABinOp=Add|Subtract|Multiply|Dividederiving(Show)dataBBinOp=And|Orderiving(Show)dataRBinOp=Greater|Lessderiving(Show)dataStmt=Seq[Stmt]|AssignStringAExpr|IfBExprStmtStmt|IdentStringderiving(Show)languageDef=emptyDef{Token.commentStart="/*",Token.commentEnd="*/",Token.commentLine="#",Token.identStart=letter,Token.identLetter=alphaNum,Token.reservedNames=["if","then","else","true","false","not","and","or"],Token.reservedOpNames=["+","-","*","/","<-","<",">","and","or","not"]}lexer=Token.makeTokenParserlanguageDefidentifier=Token.identifierlexer-- parses an identifierreserved=Token.reservedlexer-- parses a reserved namereservedOp=Token.reservedOplexer-- parses an operatorparens=Token.parenslexer-- parses surrounding parenthesis:integer=Token.integerlexer-- parses an integersemi=Token.semilexer-- parses a semicolonwhiteSpace=Token.whiteSpacelexer-- parses whitespacewhileParser::ParserStmtwhileParser=whiteSpace>>statementstatement::ParserStmtstatement=parensstatement<|>sequenceOfStmtsequenceOfStmt=dolist<-(sepBy1statement'semi)return$iflengthlist==1thenheadlistelseSeqliststatement'::ParserStmtstatement'=ifStmt<|>assignStmt<|>identNameifStmt::ParserStmtifStmt=doreserved"if"cond<-bExpressionreserved"then"stmt1<-statementreserved"else"stmt2<-statementreturn$Ifcondstmt1stmt2identName::ParserStmtidentName=dovarName<-identifierreturn$IdentvarNameassignStmt::ParserStmtassignStmt=dovar<-identifierreservedOp"<-"expr<-aExpressionreturn$AssignvarexpraExpression::ParserAExpraExpression=buildExpressionParseraOperatorsaTermbExpression::ParserBExprbExpression=buildExpressionParserbOperatorsbTermaOperators=[[Prefix(reservedOp"-">>return(Neg))],[Infix(reservedOp"*">>return(ABinaryMultiply))AssocLeft],[Infix(reservedOp"/">>return(ABinaryDivide))AssocLeft],[Infix(reservedOp"+">>return(ABinaryAdd))AssocLeft],[Infix(reservedOp"-">>return(ABinarySubtract))AssocLeft]]bOperators=[[Prefix(reservedOp"not">>return(Not))],[Infix(reservedOp"and">>return(BBinaryAnd))AssocLeft],[Infix(reservedOp"or">>return(BBinaryOr))AssocLeft]]aTerm=parensaExpression<|>liftMVaridentifier<|>liftMIntConstintegerbTerm=parensbExpression<|>(reserved"true">>return(BoolConstTrue))<|>(reserved"false">>return(BoolConstFalse))<|>rExpressionrExpression=doa1<-aExpressionop<-relationa2<-aExpressionreturn$RBinaryopa1a2relation=(reservedOp">">>returnGreater)<|>(reservedOp"<">>returnLess)parseString::String->StmtparseStringstr=caseparsewhileParser""strofLefte->error$showeRightr->rparseFile::String->IOStmtparseFilefile=doprogram<-readFilefilecaseparsewhileParser""programofLefte->printe>>fail"parse error"Rightr->returnrmain=doargs<-getArgsast<-parseFile(headargs)putStrLn$showast
But it works… we can parse. Now i have to make it more production ready and less unstable ;]. This is first attempt at this idea. I need to expand grammar and think about it more then just few minutes. but i think it is a good start. Next step for me is to make better grammar and parser and move to building interpreter of this mini language.