diff options
Diffstat (limited to 'lab2/src/main/scala/jsy')
-rw-r--r-- | lab2/src/main/scala/jsy/lab2/Lab2.scala | 150 | ||||
-rw-r--r-- | lab2/src/main/scala/jsy/lab2/Lab2.worksheet.js | 21 | ||||
-rw-r--r-- | lab2/src/main/scala/jsy/lab2/Lab2.worksheet.sc | 34 | ||||
-rw-r--r-- | lab2/src/main/scala/jsy/lab2/Parser.scala | 255 | ||||
-rw-r--r-- | lab2/src/main/scala/jsy/lab2/ast.scala | 92 | ||||
-rw-r--r-- | lab2/src/main/scala/jsy/util/JsyApplication.scala | 191 |
6 files changed, 743 insertions, 0 deletions
diff --git a/lab2/src/main/scala/jsy/lab2/Lab2.scala b/lab2/src/main/scala/jsy/lab2/Lab2.scala new file mode 100644 index 0000000..5788d74 --- /dev/null +++ b/lab2/src/main/scala/jsy/lab2/Lab2.scala @@ -0,0 +1,150 @@ +package jsy.lab2 + +object Lab2 extends jsy.util.JsyApplication { + import ast._ + /* + * CSCI 3155: Lab 2 + * Jack Sangdahl + */ + + def toNumber(v: Expr): Double = { + require(isValue(v)) + (v: @unchecked) match { + case N(n) => n + case B(b) => if (b) 1.0 else 0.0 + case S(s) => try { s.toDouble} catch { case _: NumberFormatException => Double.NaN } + case Undefined => Double.NaN + } + } + + def toBoolean(v: Expr): Boolean = { + require(isValue(v)) + (v: @unchecked) match { + case B(b) => b + case N(n) => n != 0 && !n.isNaN + case S(s) => s.nonEmpty + case Undefined => false + } + } + + def toStr(v: Expr): String = { + require(isValue(v)) + (v: @unchecked) match { + case S(s) => s + case N(n) => n.toString + case B(b) => b.toString + case Undefined => "undefined" + } + } + + /* We represent a variable environment as a map from a string of the + * variable name to the value to which it is bound. + * + * You may use the following provided helper functions to manipulate + * environments, which are just thin wrappers around the Map type + * in the Scala standard library. You can use the Scala standard + * library directly, but these are the only interfaces that you + * need. + */ + type Env = Map[String, Expr] + val empty: Env = Map() + def lookup(env: Env, x: String): Expr = env(x) + def extend(env: Env, x: String, v: Expr): Env = { + require(isValue(v)) + env + (x -> v) + } + + def eval(env: Env, e: Expr): Expr = { + e match { + // literals + case N(n) => N(n) + case B(b) => B(b) + case S(s) => S(s) + case Undefined => Undefined + + // unary ops + case Unary(Neg, e1) => N(-toNumber(eval(env, e1))) + case Unary(Not, e1) => B(!toBoolean(eval(env, e1))) + + // binary ops + case Binary(Plus, e1, e2) => N(toNumber(eval(env, e1)) + toNumber(eval(env, e2))) + case Binary(Minus, e1, e2) => N(toNumber(eval(env, e1)) - toNumber(eval(env, e2))) + case Binary(Times, e1, e2) => N(toNumber(eval(env, e1)) * toNumber(eval(env, e2))) + case Binary(Div, e1, e2) => N(toNumber(eval(env, e1)) / toNumber(eval(env, e2))) + + case Binary(And, e1, e2) => eval(env, e1) match { + case B(b1) if !b1 => B(false) + case N(n1) if n1 == 0 => N(0) + case Undefined => eval(env, e1) + case _ => eval(env, e2) + } + case Binary(Or, e1, e2) => eval(env, e1) match { + case B(b1) if b1 => B(true) + case N(n1) if n1 != 0 => N(n1) + case Undefined => eval(env, e2) + case _ => eval(env, e2) + } + + case Binary(Eq, e1, e2) => (eval(env, e1), eval(env, e2)) match { + case (N(n1), N(n2)) => B(n1 == n2) + case (B(b1), B(b2)) => B(b1 == b2) + case (S(s1), S(s2)) => B(s1 == s2) + case (Undefined, Undefined) => B(true) + case (Undefined, _) => B(false) + case (_, Undefined) => B(false) + case _ => B(false) + } + case Binary(Ne, e1, e2) => B(toNumber(eval(env, e1)) != toNumber(eval(env, e2))) + + case Binary(Lt, e1, e2) => B(toNumber(eval(env, e1)) < toNumber(eval(env, e2))) + case Binary(Le, e1, e2) => B(toNumber(eval(env, e1)) <= toNumber(eval(env, e2))) + case Binary(Gt, e1, e2) => B(toNumber(eval(env, e1)) > toNumber(eval(env, e2))) + case Binary(Ge, e1, e2) => B(toNumber(eval(env, e1)) >= toNumber(eval(env, e2))) + + case Binary(Seq, e1, e2) => + eval(env, e1) + eval(env, e2) + + case If(e1, e2, e3) => + if (toBoolean(eval(env, e1))) eval(env, e2) else eval(env, e3) + + case ConstDecl(x, e1, e2) => + val v1 = eval(env, e1) + eval(env + (x -> v1), e2) + + case Print(e1) => println(pretty(eval(env, e1))); Undefined + case Var(x) => env.getOrElse(x, throw new RuntimeException(s"Variable $x not found")) + } + } + + /* Interface to run your interpreter starting from an empty environment. This is convenient for unit testing. */ + def eval(e: Expr): Expr = { + require(closed(e)) + eval(empty, e) + } + + /* Interface to run your interpreter from a string. This is convenient for unit testing. */ + def eval(s: String): Expr = eval(Parser.parse(s)) + + // You may set this debug flag to true to get the debugging output below, but you need to turn it off to pass test cases. + // debug = true + + /* Interface to run your interpreter on an input file. This is for file-based integration tests (see Lab2JsyTests in Lab2Spec.scala). */ + def processFile(file: java.io.File): Unit = { + + if (debug) { println("Parsing ...") } + + val expr = Parser.parseFile(file) + + if (debug) { + println("\nExpression AST:\n " + expr) + println("------------------------------------------------------------") + } + + if (debug) { println("Evaluating ...") } + + val v = eval(expr) + + println(pretty(v)) + } +} diff --git a/lab2/src/main/scala/jsy/lab2/Lab2.worksheet.js b/lab2/src/main/scala/jsy/lab2/Lab2.worksheet.js new file mode 100644 index 0000000..5a1cb50 --- /dev/null +++ b/lab2/src/main/scala/jsy/lab2/Lab2.worksheet.js @@ -0,0 +1,21 @@ +/* + * CSCI 3155: Lab 2 JavaScript Worksheet + * + * This worksheet is a place to experiment with JavaScript expressions. + */ + +const five = 5; + +five + "five"; + +console.log(five), console.log("five"); + +undefined - true; + +10 * false; + +10 * undefined; + +console.log(undefined ? "then" : "else"); + +console.log("" ? "then" : "else");
\ No newline at end of file diff --git a/lab2/src/main/scala/jsy/lab2/Lab2.worksheet.sc b/lab2/src/main/scala/jsy/lab2/Lab2.worksheet.sc new file mode 100644 index 0000000..3b401ee --- /dev/null +++ b/lab2/src/main/scala/jsy/lab2/Lab2.worksheet.sc @@ -0,0 +1,34 @@ +/* + * CSCI 3155: Lab 2 Worksheet + * + * This worksheet demonstrates how you could experiment + * interactively with your implementations in Lab2.scala. + */ + +// Imports the parse and parseFile functions +import jsy.lab2.Parser.{parse, parseFile} + +// Imports the ast nodes +import jsy.lab2.ast._ + +// Imports all of the functions from your implementation +import jsy.lab2.Lab2._ + +// Call the JavaScripty parser (from the provided library) on a string +val negFourAST = parse("-4") + +// Construct an AST directly +val negFiveAST: Expr = Unary(Neg, N(5.0)) + +// Evaluate that JavaScripty expression. +//eval(negFourAST) + +// For convenience, we also have an eval function that takes a string, +// which calls the parser and then delegates to your eval function. +//eval("undefined + 1") + +// Parse the JavaScripty expression in your worksheet +val worksheetJsy = parseFile("src/main/scala/jsy/lab2/Lab2.worksheet.js") + +// Eval the JavaScripty expression in your worksheet +//eval(worksheetJsy)
\ No newline at end of file diff --git a/lab2/src/main/scala/jsy/lab2/Parser.scala b/lab2/src/main/scala/jsy/lab2/Parser.scala new file mode 100644 index 0000000..b4cf478 --- /dev/null +++ b/lab2/src/main/scala/jsy/lab2/Parser.scala @@ -0,0 +1,255 @@ +/** + * + */ +package jsy.lab2 + +import jsy.lab2.ast._ +import scala.util.parsing.combinator._ +import scala.util.parsing.input.{StreamReader} +import java.io.{InputStreamReader,FileInputStream} +import java.io.InputStream +import java.io.File +import scala.util.parsing.input.Position +import scala.util.parsing.input.NoPosition + +trait JSTokens extends token.StdTokens { + case class FloatLiteral(chars: String) extends Token { + override def toString = chars + } +} + +class Lexer extends lexical.StdLexical with JSTokens { + override def token: this.Parser[Token] = + decimal ~ opt(exponent) ^^ { + case dec ~ exp => FloatLiteral(List(Some(dec), exp).flatten.mkString) + } | + super.token + + def decimal: this.Parser[String] = + rep1(digit) ~ opt('.' ~ rep(digit)) ^^ { + case ws ~ fs => + List(Some(ws), fs map { mkList }).flatten.flatten.mkString + } + + def exponent: this.Parser[String] = + (accept('e') | accept('E')) ~ opt(accept('+') | accept('-')) ~ rep1(digit) ^^ { + case exp ~ sign ~ digits => + List(Some(List(exp)), sign map { List(_) }, Some(digits)).flatten.flatten.mkString + } +} + +trait TokenParser extends syntactical.StdTokenParsers { + type Tokens = JSTokens + val lexical = new Lexer + + import lexical.FloatLiteral + + def floatLit: this.Parser[String] = + elem("float", _.isInstanceOf[FloatLiteral]) ^^ (_.chars) +} + +object Parser extends TokenParser { + /* Lexer Set Up */ + lexical.reserved ++= List("jsy", "undefined", "true", "false", "print", "console", "log", "const", "function", "return", "number", "bool", "string", "Undefined", "null", "Null", "interface", "var", "name", "ref", "RegExp") + lexical.delimiters ++= List("-", "!", ";", ",", "+", "*", "/", "=", "===", "!==", "<", "<=", ">", ">=", "&&", "||", "(", ")", ".", "{", "}", "?", ":", "=>") + + /* Helpers */ + def seqExpr(e1: Expr, e2: Expr): Expr = Binary(Seq, e1, e2) + + /* EBNF + * + * prog ::= {stmt} + * stmt ::= block | decl | ; | expr + * block ::= '{' prog '}' + * decl ::= const x = expr + * expr ::= seq + * seq ::= cond{,cond} + * cond ::= binary [? cond : cond] + * binary ::= unary{bop(_)unary} + * unary ::= uop unary | term + * term ::= x | n | b | undefined | jsy.print(expr) | (expr) + */ + + sealed abstract class PStmt + case class ExprPStmt(e: Expr) extends PStmt + case class DeclPStmt(d: Expr => Expr) extends PStmt + case object EmpPStmt extends PStmt + + def prog: this.Parser[Expr] = + stmts ^^ (s => s(None)) + + def stmts: this.Parser[Option[Expr] => Expr] = + rep(stmt) ^^ { (stmts: List[PStmt]) => (body: Option[Expr]) => + (stmts foldRight body){ + case (EmpPStmt, eopt) => eopt + case (ExprPStmt(e), None) => Some(e) + case (ExprPStmt(e1), Some(e2)) => Some(seqExpr(e1, e2)) + case (DeclPStmt(d), None) => Some(d(Undefined)) + case (DeclPStmt(d), Some(e)) => Some(d(e)) + } match { + case None => Undefined + case Some(e) => e + } + } + + def stmt: this.Parser[PStmt] = + block ^^ ExprPStmt | + decl ^^ DeclPStmt | + expr ^^ ExprPStmt | + empty_stmt + + def empty_stmt: this.Parser[PStmt] = + ";" ^^ (_ => EmpPStmt) + + def block: this.Parser[Expr] = + "{" ~> prog <~ "}" + + def decl: this.Parser[Expr => Expr] = + ("const" ~> ident) ~ withpos("=" ~> expr) ^^ { + case x ~ ((pos,e1)) => ((e2: Expr) => ConstDecl(x, e1, e2) setPos pos) + } + + def expr: this.Parser[Expr] = + seq + + def seq: this.Parser[Expr] = + noseq ~ withposrep("," ~> noseq) ^^ { + case e0 ~ es => + (es foldRight (None: Option[(Position,Expr)])){ + case ((posi,ei), None) => Some(posi,ei) + case ((posi,ei), Some((pos,e))) => Some(posi, seqExpr(ei,e) setPos pos) + } match { + case None => e0 + case Some((pos,e)) => seqExpr(e0, e) setPos pos + } + } + + def noseq: this.Parser[Expr] = + cond + + def cond: this.Parser[Expr] = + binary(0) ~ opt(withpos(("?" ~> noseq) ~ (":" ~> noseq))) ^^ { + case e1 ~ None => e1 + case e1 ~ Some((pos, e2 ~ e3)) => If(e1, e2, e3) setPos pos + } + + val binaryOperators: Vector[List[(String, (Expr, Expr) => Expr)]] = { + def createBinaryFunction(op: Bop): (Expr, Expr) => Expr = + { Binary(op, _, _) } + Vector() ++ List( + List("||" -> createBinaryFunction(Or)), + List("&&" -> createBinaryFunction(And)), + List("===" -> createBinaryFunction(Eq), + "!==" -> createBinaryFunction(Ne)), + List("<" -> createBinaryFunction(Lt), + "<=" -> createBinaryFunction(Le), + ">" -> createBinaryFunction(Gt), + ">=" -> createBinaryFunction(Ge)), + List("+" -> createBinaryFunction(Plus), + "-" -> createBinaryFunction(Minus)), + List("*" -> createBinaryFunction(Times), + "/" -> createBinaryFunction(Div)) + ) + } + + def binary(level: Int): this.Parser[Expr] = + if (level >= binaryOperators.length) + unary + else + binary(level + 1) * bop(level) + + def bop(level: Int): this.Parser[(Expr, Expr) => Expr] = { + def doBop(opf: (String, (Expr, Expr) => Expr)): this.Parser[(Expr, Expr) => Expr] = { + val (op, f) = opf + withpos(op) ^^ { case (pos, _) => ((e1, e2) => f(e1, e2) setPos pos) } + } + val bopf0 :: bopfrest = binaryOperators(level) + bopfrest.foldLeft(doBop(bopf0))((acc, bopf) => acc | doBop(bopf)) + } + + def unary: this.Parser[Expr] = + positioned(uop ~ unary ^^ { case op ~ e => op(e) }) | + term + + def uop: this.Parser[Expr => Expr] = + "-" ^^ (_ => (e: Expr) => Unary(Neg, e)) | + "!" ^^ (_ => (e: Expr) => Unary(Not, e)) + + def term: this.Parser[Expr] = + positioned( + ident ^^ (s => Var(s)) | + floatLit ^^ (s => N(s.toDouble)) | + stringLit ^^ (s => S(s)) | + "true" ^^ (_ => B(true)) | + "false" ^^ (_ => B(false)) | + "undefined" ^^ (_ => Undefined) | + ("jsy" ~ "." ~ "print") ~> "(" ~> expr <~ ")" ^^ (e => Print(e)) | + ("console" ~ "." ~ "log") ~> "(" ~> expr <~ ")" ^^ (e => Print(e)) + ) | + "(" ~> expr <~ ")" | + "{" ~> "{" ~> prog <~ "}" <~ "}" | + failure("atomic expression expected") + + def withpos[T](q: => this.Parser[T]): this.Parser[(Position, T)] = this.Parser { in => + q(in) match { + case Success(t, in1) => Success((in.pos,t), in1) + case ns: NoSuccess => ns + } + } + + def withposrep[T](q: => this.Parser[T]): this.Parser[List[(Position,T)]] = + rep(withpos(q)) + + def withposrep1[T](q: => this.Parser[T]): this.Parser[List[(Position,T)]] = + rep1(withpos(q)) + + private var parseSource: String = "<source>" + + def formatErrorMessage(pos: Position, kind: String, msg: String): String = + if (pos != NoPosition) + "%s\n%s:%s:%s: %s\n%s".format(kind, parseSource, pos.line, pos.column, msg, pos.longString) + else + "%s\n%s: %s".format(kind, parseSource, msg) + + class SyntaxError(msg: String, next: Input) extends Exception { + override def toString = formatErrorMessage(next.pos, "SyntaxError", msg) + } + + def parseTokens(tokens: lexical.Scanner): Expr = { + phrase(prog)(tokens) match { + case Success(e, _) => e + case NoSuccess(msg, next) => throw new SyntaxError(msg, next) + case Error(msg,next) => throw new SyntaxError(msg, next) + case Failure(msg,next) => throw new SyntaxError(msg, next) + } + } + + /*** External Interface ***/ + + def formatErrorMessage(e: Expr, kind: String, msg: String): String = + formatErrorMessage(e.pos, kind, msg) + + def parse(s: String): Expr = { + parseTokens(new lexical.Scanner(s)) + } + + def parse(in: InputStream): Expr = { + val reader = StreamReader(new InputStreamReader(in)) + parseTokens(new lexical.Scanner(reader)) + } + + def parseFile(filename: String): Expr = { + parseSource = filename + parse(new FileInputStream(filename)) + } + + def parseFile(file: File): Expr = { + parseSource = file.getName + parse(new FileInputStream(file)) + } + + implicit class StringExpr(s: String) { + val e = parse(s) + def a: Expr = e + } +} diff --git a/lab2/src/main/scala/jsy/lab2/ast.scala b/lab2/src/main/scala/jsy/lab2/ast.scala new file mode 100644 index 0000000..7c8fb5e --- /dev/null +++ b/lab2/src/main/scala/jsy/lab2/ast.scala @@ -0,0 +1,92 @@ +package jsy.lab2 + +import scala.util.parsing.input.Positional + +/** + * @author Bor-Yuh Evan Chang + */ +object ast { + sealed trait Expr extends Positional // e ::= + + /* Literals */ + case class N(n: Double) extends Expr // e ::= n + case class B(b: Boolean) extends Expr // e ::= b + case class S(str: String) extends Expr // e ::= str + case object Undefined extends Expr // e ::= undefined + + /* Unary and Binary Expressions */ + case class Unary(uop: Uop, e1: Expr) extends Expr // e ::= uop e1 + case class Binary(bop: Bop, e1: Expr, e2: Expr) extends Expr // e ::= e1 bop e2 + + sealed trait Uop // uop ::= + sealed trait Bop // bop ::= + + /* Numbers */ + case object Neg extends Uop // uop ::= - + + case object Plus extends Bop // bop ::= + + case object Minus extends Bop // bop ::= - + case object Times extends Bop // bop ::= * + case object Div extends Bop // bop ::= / + + /* Booleans */ + case object Not extends Uop // uop ::= ! + case object And extends Bop // bop ::= && + case object Or extends Bop // bop ::= || + + case class If(e1: Expr, e2: Expr, e3: Expr) extends Expr // e ::= e1 ? e2 : e3 + + case object Eq extends Bop // bop ::= === + case object Ne extends Bop // bop ::= !== + case object Lt extends Bop // bop ::= < + case object Le extends Bop // bop ::= <= + case object Gt extends Bop // bop ::= > + case object Ge extends Bop // bop ::= >= + + /* Side-Effects */ + case class Print(e1: Expr) extends Expr // e ::= console.log(e1) + case object Seq extends Bop // bop ::= , + + /* Variables */ + case class Var(x: String) extends Expr // e ::= x + case class ConstDecl(x: String, e1: Expr, e2: Expr) extends Expr // e ::= const x = e1; e2 + + /* Define values. */ + def isValue(e: Expr): Boolean = e match { + case N(_) | B(_) | S(_) | Undefined => true + case _ => false + } + + /* + * Pretty-print values. + * + * We do not override the toString method so that the abstract syntax can be printed + * as is. + */ + def pretty(v: Expr): String = { + require(isValue(v)) + (v: @unchecked) match { + case N(n) => prettyNumber(n) + case B(b) => b.toString + case S(str) => str + case Undefined => "undefined" + } + } + + def prettyNumber(n: Double): String = + if (n.isWhole) "%.0f" format n else n.toString + + /* Get the free variables of e. */ + def freeVars(e: Expr): Set[String] = e match { + case Var(x) => Set(x) + case ConstDecl(x, e1, e2) => freeVars(e1) | (freeVars(e2) - x) + case N(_) | B(_) | Undefined | S(_) => Set.empty + case Unary(_, e1) => freeVars(e1) + case Binary(_, e1, e2) => freeVars(e1) | freeVars(e2) + case If(e1, e2, e3) => freeVars(e1) | freeVars(e2) | freeVars(e3) + case Print(e1) => freeVars(e1) + } + + /* Check closed expressions. */ + def closed(e: Expr): Boolean = freeVars(e).isEmpty +} diff --git a/lab2/src/main/scala/jsy/util/JsyApplication.scala b/lab2/src/main/scala/jsy/util/JsyApplication.scala new file mode 100644 index 0000000..46b658d --- /dev/null +++ b/lab2/src/main/scala/jsy/util/JsyApplication.scala @@ -0,0 +1,191 @@ +package jsy.util + +import java.io.{ByteArrayOutputStream, File} +import java.nio.file.Files + +trait JsyApplication { + var debug = false /* set to false to disable debugging output */ + var keepGoing = true /* set to true to keep going after exceptions */ + var maxSteps: Option[Int] = None /* set to a number to bound the number of execution steps */ + + /*** Implement this method to process a file. ***/ + def processFile(file: File): Unit + + var anonOption = ("input", + { case filename :: Nil => new File(filename) }: PartialFunction[List[String], File], + "A file containing a JavaScripty program or a directory with .jsy files.") + + var flagOptions = List( + ("debug", options.SetBool(b => debug = b, Some(b => debug == b)), "debugging"), + ("keep-going", options.SetBool(b => keepGoing = b, Some(b => keepGoing == b)), "keep going after exceptions"), + ("bound-steps", options.SetInt( + { i => maxSteps = i }, + Some({ + case true => maxSteps map { i => ": %d".format(i) } + case false => if (maxSteps == None) Some("") else None + })), + "bound for maximum number of execution steps before aborting") + ) + + def handle[T](default: T)(e: => T): T = + if (!keepGoing) e + else try e catch { + case exn: Throwable => println(exn.toString); default + } + + def testJsy(file: File)(k: (File, File, => (Boolean, String)) => Unit): Unit = { + val jsyext = """[.]jsy$""".r + val ans: File = new File(jsyext.replaceAllIn(file.getPath, ".ans")) + k(file, ans, { + if (!ans.exists()) { + (false, s"Expected output file ${ans} does not exist.") + } + else { + val outstream = new ByteArrayOutputStream() + Console.withOut(outstream)(handle(()){ processFile(file)}) + + val encoding = java.nio.charset.StandardCharsets.UTF_8 + val ansstring = new String(Files.readAllBytes(ans.toPath), encoding) + + outstream.flush() + val outstring = new String(outstream.toString(encoding.toString)) + outstream.close() + + (ansstring == outstring, s"Computed output does not match expected output.\nComputed:\n${outstring}\nExpected:\n${ansstring}") + } + }) + } + + def isJsy(file: File): Boolean = { + val jsyext = """[.]jsy$""".r + jsyext findFirstIn file.getName match { + case Some(_) => true + case None => false + } + } + + def doFile(doit: File => Unit, file: File) = { + def loop(file: File): Unit = { + if (file.isFile) { + doit(file) + } + else if (file.isDirectory) { + file.listFiles filter { f => f.isDirectory || isJsy(f) } foreach loop + } + else { + throw new IllegalArgumentException("File %s does not exist.".format(file)) + } + } + loop(file) + } + + def test(fileordir: File)(k: (File, File, => (Boolean, String)) => Unit): Unit = { + doFile({ f => testJsy(f)(k) }, fileordir) + } + + def main(args: Array[String]): Unit = { + val opts = new options.Options("jsy", flagOptions, anonOption) + val file: File = opts.process(args) + doFile(processFile, file) + } +} + +object options { + sealed abstract class Spec + case class SetBool(setter: Boolean => Unit, default: Option[Boolean => Boolean]) extends Spec + case class SetInt(setter: Option[Int] => Unit, default: Option[Boolean => Option[String]]) extends Spec + + class Options[T](program: String, specs: List[(String, Spec, String)], anon: (String, PartialFunction[List[String], T], String)) { + val opts: Map[String, List[String] => Option[List[String]]] = + specs.foldLeft[Map[String, List[String] => Option[List[String]]]](Map.empty)((acc, spec) => spec match { + case (name, SetBool(setter, _), _) => + acc + + (("--" + name) -> { (t: List[String]) => setter(true); Some(t) }) + + (("--no-" + name) -> { (t: List[String]) => setter(false); Some(t) }) + case (name, SetInt(setter, _), _) => + acc + + (("--" + name) -> { (t: List[String]) => t match { + case arg :: t => + val argp: Int = try arg.toInt catch { case _: Throwable => usageErr() } + setter(Some(argp)); Some(t) + case _ => usageErr() + } }) + + (("--no-" + name) -> { (t: List[String]) => setter(None); Some(t) }) + }) + + val (anonName, anonDo, anonDesc) = anon + + val nameWidth: Int = opts.foldLeft(anonName.length){ case (acc, (n, _)) => acc max (n.length + 5) } + + def padRight(s: String, w: Int): String = { + val nspaces = (w - s.length) max 0 + s + (" " * nspaces) + } + + def optline(name: String, text: String): String = { + "%-2s".format("") + padRight(name, nameWidth) + " %s%n".format(text) + } + + val descriptions: String = { + (specs foldRight "")((spec, acc) => spec match { + case (name, SetBool(_, default), desc) => { + def defaultStr(b: Boolean): String = + default map (f => if (f(b)) " (default)" else "") getOrElse("") + optline("--" + name, "turn on %s".format(desc) + defaultStr(true)) + + optline("--no-" + name, "turn off %s".format(desc) + defaultStr(false)) + + acc + } + case (name, SetInt(_, default), desc) => { + def defaultStr(b: Boolean): String = { + val opt = + for { + f <- default + s <- f(b) + } yield " (default%s)".format(s) + opt.getOrElse("") + } + optline("--" + name + " <int>", "set %s".format(desc) + defaultStr(true)) + + optline("--no-" + name, "unset %s".format(desc) + defaultStr(false)) + + acc + } + }) + } + + val header = + """ +Usage: %s [options] %s + +""".format(program, anonName) + +optline(anonName, anonDesc) + """ +Options: +""" + + private var currentArgs: List[String] = List() + + def usageErr(): Nothing = { + val unprocessed = currentArgs match { + case Nil => "" + case _ => """ +Unprocessed Arguments: + """ + currentArgs.reduceRight({ _ + " " + _ }) + "%n".format() + } + print(header + descriptions + unprocessed) + sys.exit(1) + } + + def process(args: Array[String]): T = { + def loop(l: List[String]): List[String] = { + currentArgs = l + l match { + case Nil => Nil + case h :: t => opts.get(h) match { + case None => l + case Some(doit) => { val tp = doit(t).getOrElse(t); loop(tp) } + } + } + } + val err: PartialFunction[List[String], T] = { case _ => usageErr() } + (anonDo orElse err)(loop(args.toList)) + } + } +}
\ No newline at end of file |