Disabled external gits

This commit is contained in:
2022-04-07 18:46:57 +02:00
parent 88cb3426ad
commit 15e7120d6d
5316 changed files with 4563444 additions and 6 deletions

Submodule cs452-fos/fos-assignments deleted from 74fc009d30

12
cs452-fos/fos-assignments/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.bsp
.idea/
project/project/
project/target/
target/
idea/
project/project/
project/target/
target/
.metals/
.vscode/
.bloop/

View File

@@ -0,0 +1,11 @@
.bsp
.idea/
project/project/
project/target/
target/ .bsp/
.idea/
project/project/
project/target/
target/
.metals/
.vscode/

View File

@@ -0,0 +1,4 @@
scalaVersion := "3.0.2"
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"

View File

@@ -0,0 +1 @@
sbt.version=1.5.5

View File

@@ -0,0 +1,136 @@
package fos
import scala.util.parsing.combinator.syntactical.StandardTokenParsers
import scala.util.parsing.input.*
/** This object implements a parser and evaluator for the NB
* language of booleans and numbers found in Chapter 3 of
* the TAPL book.
*/
object Arithmetic extends StandardTokenParsers {
lexical.reserved ++= List("true", "false", "if", "then", "else", "succ", "pred", "iszero")
import lexical.NumericLit
/** term ::= 'true'
| 'false'
| 'if' term 'then' term 'else' term
| numericLit
| 'succ' term
| 'pred' term
| 'iszero' term
*/
def NumericRec(c:Int,s:Term): (Int,Term) = {
if(c<=0){
(0,s)
}else{
NumericRec(c-1,Succ(s))
}
}
def term: Parser[Term] = {
"true" ^^^ True |
"false" ^^^ False |
("if" ~> term) ~ ("then" ~> term) ~ ("else" ~> term) ^^ { case cond ~ t1 ~ t2 => If(cond, t1, t2) } |
numericLit ^^ { case v if (v.toInt >=0) => NumericRec(v.toInt,Zero)._2} |
("succ" ~> term) ^^ { Succ(_) } |
("pred" ~> term) ^^ { Pred(_) } |
("iszero" ~> term) ^^ { IsZero(_) } |
failure("Unknown Term")
}
case class NoReductionPossible(t: Term) extends Exception(t.toString)
/** Return a list of at most n terms, each being one step of reduction. */
def path(t: Term, n: Int = 64): List[Term] =
if (n <= 0) Nil
else
t :: {
try {
path(reduce(t), n - 1)
} catch {
case NoReductionPossible(t1) =>
Nil
}
}
def isNumeric(t: Term): Boolean = t match {
case Zero => true
case Pred(v) => isNumeric(v)
case Succ(v) => isNumeric(v)
case _ => false;
}
/** Perform one step of reduction, when possible.
* If reduction is not possible NoReductionPossible exception
* with corresponding irreducible term should be thrown.
*/
def reduce(t: Term): Term = t match {
case If(cond, t1, t2) => if(cond == True) t1 else if (cond == False) t2 else If(reduce(cond), t1, t2)
case IsZero(z) => z match {
case Zero => True
case Succ(_) => False
case other => IsZero(reduce(other))
}
case Succ(s) => s match {
case Zero => throw NoReductionPossible(Zero)
case other => Succ(reduce(other))
}
case Pred(p) => p match {
case Zero => Zero
case Succ(nv) if (isNumeric(nv)) => nv
case other => Pred(reduce(other))
}
case other => throw NoReductionPossible(other)
}
case class TermIsStuck(t: Term) extends Exception(t.toString)
/** Perform big step evaluation (result is always a value.)
* If evaluation is not possible TermIsStuck exception with
* corresponding inner irreducible term should be thrown.
*/
def eval(t: Term): Term = t match {
case True => True
case False => False
case If(cond, t1, t2) => eval(cond) match {
case True => eval(t1)
case False => eval(t2)
case _ => throw TermIsStuck(t)
}
case Zero => Zero
case Succ(v) => eval(v) match {
case Zero => Zero
case Pred(nv) => eval(nv)
case _ => throw TermIsStuck(t)
}
case Pred(v) => eval(v) match {
case Zero => Zero
case Succ(nv) => eval(nv)
case _ => throw TermIsStuck(t)
}
case IsZero(v) => eval(v) match {
case Zero => True
case Succ(nv) => False
case _ => throw TermIsStuck(t)
}
}
def main(args: Array[String]): Unit = {
val stdin = new java.io.BufferedReader(new java.io.InputStreamReader(System.in))
val tokens = new lexical.Scanner(stdin.readLine())
phrase(term)(tokens) match {
case Success(trees, _) =>
for (t <- path(trees))
println(t)
try {
print("Big step: ")
println(eval(trees))
} catch {
case TermIsStuck(t) => println("Stuck term: " + t)
}
case e =>
println(e)
}
}
}

View File

@@ -0,0 +1,13 @@
package fos
import scala.util.parsing.input.Positional
/** Abstract Syntax Trees for terms. */
sealed abstract class Term extends Positional
case object True extends Term
case object False extends Term
case class If(cond: Term, t1: Term, t2: Term) extends Term
case object Zero extends Term
case class Succ(t: Term) extends Term
case class Pred(t: Term) extends Term
case class IsZero(t: Term) extends Term

View File

@@ -0,0 +1,4 @@
scalaVersion := "3.0.2"
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"

View File

@@ -0,0 +1 @@
sbt.version=1.5.5

Binary file not shown.

View File

@@ -0,0 +1,9 @@
package fos
import scala.util.parsing.input.Positional
/** Abstract Syntax Trees for terms. */
sealed abstract class Term extends Positional
case class Var(name: String) extends Term
case class Abs(v: String, t: Term) extends Term
case class App(t1: Term, t2: Term) extends Term

View File

@@ -0,0 +1,149 @@
package fos
import scala.util.parsing.combinator.syntactical.StandardTokenParsers
import scala.util.parsing.input._
/** This object implements a parser and evaluator for the
* untyped lambda calculus found in Chapter 5 of
* the TAPL book.
*/
object Untyped extends StandardTokenParsers {
lexical.delimiters ++= List("(", ")", "\\", ".")
import lexical.Identifier
/** t ::= x
| '\' x '.' t
| t t
| '(' t ')'
*/
def term: Parser[Term] =
rep1(v) ^^ {
case x::Nil => x
case l => l.reduceLeft(App.apply)
}
def v: Parser[Term] =
(ident) ^^ {Var.apply} |
("\\" ~> ident) ~ ("." ~> term) ^^ { case x ~ t => Abs(x,t) } |
( "(" ~> term <~ ")") |
failure("Unmatched Term")
/** <p>
* Alpha conversion: term <code>t</code> should be a lambda abstraction
* <code>\x. t</code>.
* </p>
* <p>
* All free occurences of <code>x</code> inside term <code>t/code>
* will be renamed to a unique name.
* </p>
*
* @param t the given lambda abstraction.
* @return the transformed term with bound variables renamed.
*/
val r = new scala.util.Random(31)
def alpha(t: Abs): Abs = t match {
case Abs(x,lt) =>
val uid = x.toString()+"_"+r.nextString(10)
Abs(uid, recurseUID(x, lt, uid))
}
def recurseUID(x: String, t: Term, uid: String): Term = t match {
case Var(v) => if (v == x) Var(uid) else t
case Abs(v, lt) => if (v == x) Abs(uid, recurseUID(x, lt, uid))
else Abs(v, recurseUID(x, lt, uid))
case App(lt1, lt2) => App(recurseUID(x, lt1, uid), recurseUID(x, lt2, uid))
}
/** Straight forward substitution method
* (see definition 5.3.5 in TAPL book).
* [x -> s]t
*
* @param t the term in which we perform substitution
* @param x the variable name
* @param s the term we replace x with
* @return ...
*/
def subst(t: Term, x: String, s: Term): Term = t match {
case Var(v) => if (v == x) s else t
case t@Abs(v, lt) => if (v == x) t
else if (FV(s).contains(v)) subst(alpha(t), x, s)
else Abs(v, subst(lt, x, s))
case App(lt1, lt2) => App(subst(lt1, x, s), subst(lt2, x, s))
}
def FV(t: Term): List[String] = t match {
case Var(v) => List(v)
case Abs(v, lt) => FV(lt).filterNot(_==v)
case App(lt1, lt2) => FV(lt1):::FV(lt2)
}
/** Term 't' does not match any reduction rule. */
case class NoReductionPossible(t: Term) extends Exception(t.toString)
/** Normal order (leftmost, outermost redex first).
*
* @param t the initial term
* @return the reduced term
*/
def reduceNormalOrder(t: Term): Term = t match {
case App(Abs(v, t1), t2) => subst(t1, v, t2)
case App(t1, t2) if isReducible(t1, reduceNormalOrder) => App(reduceNormalOrder(t1), t2)
case App(t1, t2) if isReducible(t2, reduceNormalOrder) => App(t1, reduceNormalOrder(t2))
case Abs(v, t1) if isReducible(t1, reduceNormalOrder) => Abs(v, reduceNormalOrder(t1))
case _ => throw NoReductionPossible(t)
}
/**
* Is this term reducible?
* @param term
* @return whether term is reducible
*/
def isReducible(term: Term, reduce: Term => Term): Boolean = try {
reduce(term)
true
} catch { _ =>
false
}
/** Call by value reducer. */
def reduceCallByValue(t: Term): Term = t match {
case App(Abs(v, t1), t2: Abs) => subst(t1, v, t2)
case App(t1, t2) if isReducible(t1, reduceCallByValue) => App(reduceCallByValue(t1), t2)
case App(t1: Abs, t2) if isReducible(t2, reduceCallByValue) => App(t1, reduceNormalOrder(t2))
case _ => throw NoReductionPossible(t)
}
/** Returns a stream of terms, each being one step of reduction.
*
* @param t the initial term
* @param reduce the method that reduces a term by one step.
* @return the stream of terms representing the big reduction.
*/
def path(t: Term, reduce: Term => Term): LazyList[Term] =
try {
var t1 = reduce(t)
LazyList.cons(t, path(t1, reduce))
} catch {
case NoReductionPossible(_) =>
LazyList.cons(t, LazyList.empty)
}
def main(args: Array[String]): Unit = {
val stdin = new java.io.BufferedReader(new java.io.InputStreamReader(System.in))
val tokens = new lexical.Scanner(stdin.readLine())
phrase(term)(tokens) match {
case Success(trees, _) =>
println("normal order: ")
for (t <- path(trees, reduceNormalOrder))
println(t)
println("call-by-value: ")
for (t <- path(trees, reduceCallByValue))
println(t)
case e =>
println(e)
}
}
}

View File

@@ -0,0 +1,4 @@
scalaVersion := "3.0.2"
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"

View File

@@ -0,0 +1 @@
sbt.version=1.5.5

View File

@@ -0,0 +1,6 @@
// DO NOT EDIT! This file is auto-generated.
// This file enables sbt-bloop to create bloop config files.
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.10-8-8d1cbc4f")

View File

@@ -0,0 +1,6 @@
// DO NOT EDIT! This file is auto-generated.
// This file enables sbt-bloop to create bloop config files.
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.10-8-8d1cbc4f")

Binary file not shown.

View File

@@ -0,0 +1,297 @@
package fos
import scala.util.parsing.combinator.syntactical.StandardTokenParsers
import scala.util.parsing.combinator.Parsers
import scala.util.parsing.input._
/** This object implements a parser and evaluator for the
* simply typed lambda calculus found in Chapter 9 of
* the TAPL book.
*/
object SimplyTyped extends StandardTokenParsers {
lexical.delimiters ++= List("(", ")", "\\", ".", ":", "=", "->", "{", "}", ",", "*")
lexical.reserved ++= List("Bool", "Nat", "true", "false", "if", "then", "else", "succ",
"pred", "iszero", "let", "in", "fst", "snd")
/** t ::= "true"
* | "false"
* | number
* | "succ" t
* | "pred" t
* | "iszero" t
* | "if" t "then" t "else" t
* | ident
* | "\" ident ":" T "." t
* | t t
* | "(" t ")"
* | "let" ident ":" T "=" t "in" t
* | "{" t "," t "}"
* | "fst" t
* | "snd" t
*/
def term: Parser[Term] =
rep1(v) ^^ {
case x::Nil => x
case l => l.reduceLeft(App.apply)
}
def v: Parser[Term] =
"true" ^^^ True |
"false" ^^^ False |
numericLit ^^ { case n => numericRec(n.toInt,Zero)._2 } |
("succ" ~> term) ^^ { Succ.apply } |
("pred" ~> term) ^^ { Pred.apply } |
("iszero" ~> term) ^^ { IsZero.apply } |
("if" ~> term) ~ ("then" ~> term) ~ ("else" ~> term) ^^ { case cond ~ t1 ~ t2 => If(cond, t1, t2) } |
ident ^^ { Var.apply } |
("\\" ~> ident) ~ (":" ~> typeTerm) ~ ("." ~> term) ^^ { case id ~ tpe ~ t => Abs(id, tpe, t) } |
("(" ~> term <~ ")") |
("let" ~> ident) ~ (":" ~> typeTerm) ~ ("=" ~> term) ~ ("in" ~> term) ^^ { case id ~ tpe ~ t1 ~ t2 => App(Abs(id, tpe, t2), t1) } |
("{" ~> term <~ ",") ~ (term <~ "}") ^^ { case f ~ s => TermPair(f, s) } |
("fst" ~> term) ^^ { First.apply } |
("snd" ~> term) ^^ { Second.apply } |
failure("Unmatched Term")
def baseTypeTerm: Parser[Type] =
"Bool" ^^^ TypeBool |
"Nat" ^^^ TypeNat |
("(" ~> typeTerm <~ ")")
def pairTypeTerm: Parser[Type]=
rep1sep(baseTypeTerm, "*") ^^ { case p => p.reduceRight(TypePair.apply) }
def typeTerm: Parser[Type]=
rep1sep(pairTypeTerm, "->") ^^ { case p => p.reduceRight(TypeFun.apply) }
def numericRec(c:Int,s:Term): (Int,Term) = {
if(c<=0) (0,s)
else numericRec(c-1,Succ(s))
}
def isNumeric(t: Term): Boolean = t match {
case Zero => true
case Pred(v) => isNumeric(v)
case Succ(v) => isNumeric(v)
case _ => false;
}
def isReducible(t: Term): Boolean = try {
reduce(t)
true
} catch { _ =>
false
}
/** Thrown when no reduction rule applies to the given term. */
case class NoRuleApplies(t: Term) extends Exception(t.toString)
/** Print an error message, together with the position where it occured. */
case class TypeError(t: Term, msg: String) extends Exception(msg) {
override def toString =
msg + "\n" + t
}
/** The context is a list of variable names paired with their type. */
type Context = List[(String, Type)]
/** <p>
* Alpha conversion: term <code>t</code> should be a lambda abstraction
* <code>\x. t</code>.
* </p>
* <p>
* All free occurences of <code>x</code> inside term <code>t/code>
* will be renamed to a unique name.
* </p>
*
* @param t the given lambda abstraction.
* @return the transformed term with bound variables renamed.
*/
//val r = new scala.util.Random(31)
var count: Int = 0
def alpha(t: Abs): Abs = t match {
case Abs(x,tp, t) =>
val uid = x.toString()+"_"+count
count += 1
Abs(uid, tp, recurseUID(x, t, uid))
}
def recurseUID(x: String, t: Term, uid: String): Term = t match {
case True | False | Zero => t
case Var(v) => if (v == x) Var(uid) else t
case Abs(v, tp, t) => if(v==x) Abs(uid, tp, recurseUID(x, t, uid))
else Abs(v, tp, recurseUID(x, t, uid))
case App(t1, t2) => App(recurseUID(x, t1, uid), recurseUID(x, t2, uid))
case Succ(t) => Succ(recurseUID(x, t, uid))
case Pred(t) => Pred(recurseUID(x, t, uid))
case IsZero(t) => IsZero(recurseUID(x, t,uid))
case If(cond, t1, t2) => If(recurseUID(x, cond, uid), recurseUID(x, t1, uid), recurseUID(x, t2, uid))
case TermPair(t1, t2) => TermPair(recurseUID(x, t1, uid), recurseUID(x, t2,uid))
case First(t) => First(recurseUID(x, t, uid))
case Second(t) => Second(recurseUID(x, t, uid))
}
def FV(t: Term): List[String] = t match {
case True | False | Zero => List.empty
case Var(v) => List(v)
case Abs(v, tp, lt) => FV(lt).filterNot(_==v)
case App(lt1, lt2) => FV(lt1):::FV(lt2)
case Succ(t) => FV(t)
case Pred(t) => FV(t)
case IsZero(t) => FV(t)
case If(cond,t1,t2) => FV(cond):::FV(t1):::FV(t2)
case TermPair(t1,t2) => FV(t1):::FV(t2)
case First(t) => FV(t)
case Second(t) => FV(t)
}
def subst(t: Term, x: String, s: Term): Term = t match {
case True | False | Zero => t
case Var(v) if (v==x) => s
case Var(_) => t
case If(t1, t2, t3) => If(subst(t1,x,s), subst(t2,x,s), subst(t3,x,s))
case Pred(t) => Pred(subst(t,x,s))
case Succ(t) => Succ(subst(t,x,s))
case IsZero(t) => IsZero(subst(t,x,s))
case t@Abs(v, tp, lt) => if (v==x) t
else if (FV(s).contains(v)) subst(alpha(t),x,s)
else Abs(v,tp,subst(lt,x,s))
case App(t1, t2) => App(subst(t1,x,s), subst(t2,x,s))
case TermPair(t1, t2) => TermPair(subst(t1,x,s), subst(t2,x,s))
case First(t) => First(subst(t,x,s))
case Second(t) => Second(subst(t,x,s))
}
/** Call by value reducer. */
def reduce(t: Term): Term = t match {
case If(True, t1, t2) => t1
case If(False, t1, t2) => t2
case If(cond, t1, t2) => If(reduce(cond), t1, t2)
case IsZero(Zero) => True
case IsZero(Succ(v)) if isNumeric(v) => False
case IsZero(t) => IsZero(reduce(t))
case Pred(Zero) => Zero
case Pred(Succ(v)) if isNumeric(v) => v
case Pred(v) => Pred(reduce(v))
case Succ(t) => Succ(reduce(t))
case App(t1,t2) if isReducible(t1) => App(reduce(t1),t2)
case App(t1,t2) if isReducible(t2) => App(t1,reduce(t2))
case App(Abs(x, tp, t1), t2) => subst(t1,x,t2)
case First(TermPair(t,_)) if !isReducible(t) => t
case First(t) => First(reduce(t))
case Second(TermPair(_,t)) if !isReducible(t) => t
case Second(t) => Second(reduce(t))
case TermPair(t1,t2) if isReducible(t1) => TermPair(reduce(t1), t2)
case TermPair(t1,t2) if isReducible(t2) => TermPair(t1, reduce(t2))
case _ => throw NoRuleApplies(t)
}
/** Returns the type of the given term <code>t</code>.
*
* @param ctx the initial context
* @param term the given term
* @return the computed type
*/
def typeof(ctx: Context, term: Term): Type =
term match {
case True | False => TypeBool
case Zero => TypeNat
case Succ(t) => {
val innerType = typeof(ctx, t)
if(innerType == TypeNat) TypeNat
else throw TypeError(term, f"Not typeable, Should be Nat, is $innerType")
}
case Pred(t) => {
val innerType = typeof(ctx, t)
if(innerType == TypeNat) TypeNat
else throw TypeError(term, f"Not typeable, Should be Nat, is $innerType")
}
case IsZero(t) => {
val innerType = typeof(ctx, t)
if(innerType == TypeNat) TypeBool
else throw TypeError(term, f"Not typeable, Should be Nat, is $innerType")
}
case If(cond, t1, t2) => {
val condType = typeof(ctx, cond)
val ifType = typeof(ctx, t1)
val elseType = typeof(ctx, t2)
if(condType == TypeBool && ifType == elseType) ifType
else throw TypeError(term, f"Not typeable, If not consistent: $condType $ifType, $elseType")
}
case Var(name) => ctx.find { _._1 == name } match {
case Some(_, t) => t
case None => throw TypeError(term, f"Not typeable, $name not found")
}
case TermPair(t1, t2) => TypePair(typeof(ctx, t1), typeof(ctx, t2))
case First(t) => typeof(ctx,t) match {
case TypePair(t1, t2) => t1
case _ => throw TypeError(term, f"Not typeable. Should be Pair, found ${typeof(ctx, t)}")
}
case Second(t) => typeof(ctx,t) match {
case TypePair(t1, t2) => t2
case _ => throw TypeError(term, f"Not typeable. Should be Pair, found ${typeof(ctx, t)}")
}
case App(t1, t2) => {
val t2type = typeof(ctx, t2)
val t1type = typeof(ctx, t1)
t1type match {
case TypeFun(funt1, funt2) if funt1 == t2type => funt2
case TypeFun(funt1, funt2) => throw TypeError(term, f"Not typeable. Should be $funt1, found $t2type")
case _ => throw TypeError(term, f"Not typeable. Should be Fun, found $t1type")
}
}
case Abs(x, t1, t) => {
val newContext: Context = ctx.appended((x, t1))
TypeFun(t1, typeof(newContext, t))
}
case _ => throw TypeError(term, f"Unexpected Type: ${typeof(ctx,term)}")
}
/** Returns a stream of terms, each being one step of reduction.
*
* @param t the initial term
* @param reduce the evaluation strategy used for reduction.
* @return the stream of terms representing the big reduction.
*/
def path(t: Term, reduce: Term => Term): LazyList[Term] =
try {
var t1 = reduce(t)
LazyList.cons(t, path(t1, reduce))
} catch {
case NoRuleApplies(_) =>
LazyList.cons(t, LazyList.empty)
}
def main(args: Array[String]): Unit = {
val stdin = new java.io.BufferedReader(new java.io.InputStreamReader(System.in))
val tokens = new lexical.Scanner(stdin.readLine())
phrase(term)(tokens) match {
case Success(trees, _) =>
try {
println("typed: " + typeof(Nil, trees))
for (t <- path(trees, reduce))
println(t)
} catch {
case tperror: Exception => println(tperror.toString)
}
case e =>
println(e)
}
}
}

View File

@@ -0,0 +1,72 @@
package fos
import scala.util.parsing.input.Positional
/** Abstract Syntax Trees for terms. */
sealed abstract class Term extends Positional
object True extends Term {
override def toString() = "true"
}
object False extends Term {
override def toString() = "false"
}
case object Zero extends Term {
override def toString() = "0"
}
case class Succ(t: Term) extends Term {
override def toString() = "succ " + t
}
case class Pred(t: Term) extends Term {
override def toString() = "pred " + t
}
case class IsZero(t: Term) extends Term {
override def toString() = "iszero " + t
}
case class If(cond: Term, t1: Term, t2: Term) extends Term {
override def toString() = "if " + cond + " then " + t1 + " else " + t2
}
case class Var(name: String) extends Term {
override def toString() = name
}
case class Abs(v: String, tp: Type, t: Term) extends Term {
override def toString() = "(\\" + v + ":" + tp + "." + t + ")"
}
case class App(t1: Term, t2: Term) extends Term {
override def toString() = t1.toString + (t2 match {
case App(_, _) => " (" + t2.toString + ")" // left-associative
case _ => " " + t2.toString
})
}
case class TermPair(t1: Term, t2: Term) extends Term {
override def toString() = "{" + t1 + "," + t2 + "}"
}
case class First(t: Term) extends Term {
override def toString() = "fst " + t
}
case class Second(t: Term) extends Term {
override def toString() = "snd " + t
}
/** Abstract Syntax Trees for types. */
abstract class Type extends Positional
case object TypeBool extends Type {
override def toString() = "Bool"
}
case object TypeNat extends Type {
override def toString() = "Nat"
}
case class TypeFun(t1: Type, t2: Type) extends Type {
override def toString() = (t1 match {
case TypeFun(_, _) => "(" + t1 + ")" // right-associative
case _ => t1.toString
}) + "->" + t2
}
case class TypePair(t1: Type, t2: Type) extends Type {
override def toString() = "(" + t1 + " * " + t2 + ")"
}

View File

@@ -0,0 +1,280 @@
#!/usr/bin/env python3
# This script was contributed by Timothée Loyck Andres.
import argparse
import json
import os.path
import pathlib
import shutil
import ssl
import tempfile
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from getpass import getpass
from smtplib import SMTP_SSL, SMTPAuthenticationError, SMTPDataError
from typing import List, Optional, Any, Union
# ========================= CONFIG VARIABLES =========================
SMTP_SERVER = 'mail.epfl.ch'
BOT_EMAIL_ADDRESS = 'lamp.fos.bot@gmail.com'
# The minimum number of people in a group
MIN_SCIPERS = 1
# The maximum number of people in a group
MAX_SCIPERS = 3
# The number of the project (set to None to prompt user)
PROJECT_NUMBER = None
# The name of the file in which the submission configuration is stored
CONFIG_FILE_NAME = '.submission_info.json'
# The name of the folder to be zipped
SRC_FOLDER = 'src'
# ====================================================================
project_path = pathlib.Path(__file__).parent.resolve()
config_path = f'{project_path}/{CONFIG_FILE_NAME}'
src_path = f'{project_path}/{SRC_FOLDER}'
tmp_folder = tempfile.gettempdir()
class ConfigData:
def __init__(self, data_values: Optional[dict] = None, **kwargs):
"""
Creates a new data object with the given values. A dictionary may be passed, or keyword arguments for each data
piece.
Contains: email address, username, SCIPER numbers of the group's members, project number
:argument data_values: an optional dictionary containing the required data
:keyword email: the email of the user
:keyword username: the GASPAR id of the user
:keyword scipers: the list of SCIPER numbers of the group's members
:keyword project_num: the project's number
"""
if data_values is not None:
data = data_values
elif len(kwargs) > 0:
data = kwargs
else:
data = dict()
self.email: str = data.get('email')
self.username: str = data.get('username')
self.scipers: List[str] = data.get('scipers')
self.project_num: int = data.get('project_num')
def get_config_data(self) -> dict:
return {
'email': self.email,
'username': self.username,
'scipers': self.scipers,
'project_num': self.project_num
}
def get_scipers() -> List:
"""
Retrieves the group's SCIPER numbers.
:return: a list containing the SCIPER numbers of all the members of the FoS group
"""
def is_sciper(string: str) -> bool:
try:
return len(string) == 6 and int(string) > 0
except TypeError:
return False
num_scipers = None
scipers: List[str] = []
while num_scipers is None:
num_scipers = get_sanitized_input(
"Number of people in the group: ",
int,
predicate=lambda n: MIN_SCIPERS <= n <= MAX_SCIPERS,
predicate_error_msg=f"The number of people must be between {MIN_SCIPERS} and {MAX_SCIPERS} included."
)
for i in range(num_scipers):
sciper = None
while sciper is None:
sciper = get_sanitized_input(
f"SCIPER {i + 1}: ",
predicate=is_sciper,
predicate_error_msg="Invalid SCIPER number. Please try again."
)
scipers.append(sciper)
return scipers
def get_sanitized_input(prompt: str, value_type: type = str, **kwargs) -> Optional[Any]:
"""
Sanitizes the user's input.
:param prompt: the message to be displayed for the user
:param value_type: the type of value that we expect, for example str or int
:keyword allow_empty: allow the input to be empty. The returned string may be the empty string
:keyword predicate: a function that, when applied to the sanitized input, checks if it is valid
:keyword predicate_error_msg: a message to be displayed if the predicate returns false on the input
:return: the input as the passed type, or None if the input contained only whitespaces or if the type cast failed
"""
str_value = input(prompt).strip()
if len(str_value) > 0 or kwargs.get('allow_empty'):
try:
value = value_type(str_value)
p = kwargs.get('predicate')
if p is not None and not p(value):
if kwargs.get('predicate_error_msg') is None:
print("Invalid value. Please try again.")
elif len(kwargs.get('predicate_error_msg')) > 0:
print(kwargs.get('predicate_error_msg'))
return None
return value
except TypeError:
raise TypeError(f"Incorrect value type: {value_type}")
except ValueError:
print(f"The value could not be interpreted as type {value_type.__name__}. Please try again.")
return None
def get_config(from_file: bool = True) -> ConfigData:
"""
Retrieves the configuration for sending the email. It may be fetched from a configuration file, or if it does not
exist or is incomplete, it will ask the user for the data, then write it to the config file.
:param from_file: whether to retrieve the configuration from the config file if it exists. Default is True
:return: the configuration to use for the email
"""
data = ConfigData()
# Set project number if it is already specified
data.project_num = PROJECT_NUMBER
if from_file and not os.path.isfile(config_path):
print('Please provide data that will be used to submit your project.')
print(f'This information (sans the password) will be saved in: ./{CONFIG_FILE_NAME}')
if from_file and os.path.isfile(config_path):
with open(config_path, 'r') as config_file:
config = json.load(config_file)
if type(config) is dict:
data = ConfigData(config)
if data.scipers is None:
data.scipers = get_scipers()
while data.email is None:
data.email = get_sanitized_input("Email address: ", predicate=lambda address: '@' in address)
while data.username is None:
data.username = get_sanitized_input("Gaspar ID: ")
while data.project_num is None:
data.project_num = get_sanitized_input("Project number: ", int, predicate=lambda n: n > 0)
set_config(data)
return data
def set_config(data: ConfigData) -> None:
"""
Saves the configuration in the config file.
:param data: the data to be saved
"""
with open(config_path, 'w') as config_file:
json.dump(data.get_config_data(), config_file)
def create_email(frm: str, to: str, subject: str, content: Optional[str] = None,
attachments: Optional[Union[str, List[str]]] = None) -> MIMEMultipart:
"""
Creates an email.
:param frm: the address from which the email is sent
:param to: the address to which send the email
:param subject: the subject of the email
:param content: the content of the email. Can be empty
:param attachments: the attachments of the email. Can be a path or a list of paths
"""
message = MIMEMultipart()
message['From'] = frm
message['To'] = to
message['Subject'] = subject
if content is not None:
# Add content into body of message
message.attach(MIMEText(content, 'plain'))
if attachments is not None:
if type(attachments) is str:
attachments = [attachments]
for attachment_path in attachments:
part = MIMEBase("application", "octet-stream")
with open(attachment_path, 'rb') as attachment:
part.set_payload(attachment.read())
encoders.encode_base64(part)
part.add_header("Content-Disposition", f"attachment; filename={os.path.basename(attachment_path)}")
message.attach(part)
return message
if __name__ == '__main__':
arg_parser = argparse.ArgumentParser(description="Submits the project to the bot for grading.", allow_abbrev=False)
arg_parser.add_argument('-r', '--reset', action='store_true', help="ask for the submission data even if previously "
"specified")
arg_parser.add_argument('-s', '--self', action='store_true', help="send the mail to yourself instead of the bot")
if not os.path.isdir(src_path):
arg_parser.exit(1, f"No {SRC_FOLDER} folder found. Aborting.\n")
args = arg_parser.parse_args()
config: ConfigData = get_config(from_file=not args.reset)
password: str = getpass("Gaspar password: ")
recipient = config.email if args.self else BOT_EMAIL_ADDRESS
mail = create_email(
config.email,
recipient,
f"Project {config.project_num} ({', '.join(config.scipers)})",
attachments=shutil.make_archive(f'{tmp_folder}/{SRC_FOLDER}', 'zip', root_dir=project_path,
base_dir=f'{SRC_FOLDER}')
)
with SMTP_SSL(SMTP_SERVER, context=ssl.create_default_context()) as server:
try:
server.login(config.username, password)
server.sendmail(config.email, recipient, mail.as_string())
print(f"Submission sent to {recipient}.")
except SMTPAuthenticationError as e:
if e.smtp_code == 535:
print(f"Wrong GASPAR ID ({config.username}) or password. Your ID will be asked for again on the next"
" run.")
# Remove (potentially) incorrect ID from config
config.username = None
set_config(config)
exit(2)
else:
raise
except SMTPDataError as e:
if e.smtp_code == 550:
print("You email address seems to be incorrect. It will be asked for again on the next run.")
# Remove incorrect address from config
config.email = None
set_config(config)
exit(2)
else:
raise

View File

@@ -0,0 +1,4 @@
scalaVersion := "3.0.2"
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"

View File

@@ -0,0 +1 @@
sbt.version=1.5.5

Binary file not shown.

View File

@@ -0,0 +1,363 @@
package fos
import scala.util.parsing.combinator.syntactical.StandardTokenParsers
import scala.util.parsing.input._
/** This object implements a parser and evaluator for the
* simply typed lambda calculus found in Chapter 9 of
* the TAPL book.
*/
object SimplyTypedExtended extends StandardTokenParsers {
lexical.delimiters ++= List("(", ")", "\\", ".", ":", "=", "->", "{", "}", ",", "*", "+",
"=>", "|")
lexical.reserved ++= List("Bool", "Nat", "true", "false", "if", "then", "else", "succ",
"pred", "iszero", "let", "in", "fst", "snd", "fix", "letrec",
"case", "of", "inl", "inr", "as")
/** t ::= "true"
* | "false"
* | number
* | "succ" t
* | "pred" t
* | "iszero" t
* | "if" t "then" t "else" t
* | ident
* | "\" ident ":" T "." t
* | t t
* | "(" t ")"
* | "let" ident ":" T "=" t "in" t
* | "{" t "," t "}"
* | "fst" t
* | "snd" t
* | "inl" t "as" T
* | "inr" t "as" T
* | "case" t "of" "inl" ident "=>" t "|" "inr" ident "=>" t
* | "fix" t
* | "letrec" ident ":" T "=" t "in" t
*/
def term: Parser[Term] =
rep1(v) ^^ {
case x::Nil => x
case l => l.reduceLeft(App.apply)
}
def v: Parser[Term] =
"true" ^^^ True |
"false" ^^^ False |
numericLit ^^ { case n => numericRec(n.toInt,Zero)._2 } |
("succ" ~> term) ^^ { Succ.apply } |
("pred" ~> term) ^^ { Pred.apply } |
("iszero" ~> term) ^^ { IsZero.apply } |
("if" ~> term) ~ ("then" ~> term) ~ ("else" ~> term) ^^ { case cond ~ t1 ~ t2 => If(cond, t1, t2) } |
ident ^^ { Var.apply } |
("\\" ~> ident) ~ (":" ~> typeTerm) ~ ("." ~> term) ^^ { case id ~ tpe ~ t => Abs(id, tpe, t) } |
("(" ~> term <~ ")") |
("let" ~> ident) ~ (":" ~> typeTerm) ~ ("=" ~> term) ~ ("in" ~> term) ^^ { case id ~ tpe ~ t1 ~ t2 => App(Abs(id, tpe, t2), t1) } |
("{" ~> term <~ ",") ~ (term <~ "}") ^^ { case f ~ s => TermPair(f, s) } |
("fst" ~> term) ^^ { First.apply } |
("snd" ~> term) ^^ { Second.apply } |
("inl" ~> term) ~ ("as" ~> typeTerm) ^^ { case t~tpe => Inl(t,tpe) } |
("inr" ~> term) ~ ("as" ~> typeTerm) ^^ { case t~tpe => Inr(t,tpe) } |
("case" ~> term) ~ ("of" ~> "inl" ~> ident) ~ ("=>" ~> term) ~
("|" ~> "inr" ~> ident) ~ ("=>" ~> term) ^^ { case t~x1~t1~x2~t2 => Case(t,x1,t1,x2,t2) } |
("fix" ~> term) ^^ { Fix.apply } |
("letrec" ~> ident) ~ (":" ~> typeTerm) ~ ("=" ~> term) ~ ("in" ~> term) ^^ { case id~tpe~t1~t2 => App(Abs(id,tpe,t2),Fix(Abs(id,tpe,t1)))} |
failure("Unmatched Term")
def baseTypeTerm: Parser[Type] =
"Bool" ^^^ TypeBool |
"Nat" ^^^ TypeNat |
("(" ~> typeTerm <~ ")")
def pairTypeTerm: Parser[Type]=
rep1sep(baseTypeTerm, "*") ^^ { case p => p.reduceRight(TypePair.apply) }
def sumTypeTerm: Parser[Type]=
rep1sep(baseTypeTerm, "+") ^^ { case p => p.reduceRight(TypeSum.apply) }
def typeTerm: Parser[Type]=
rep1sep(pairTypeTerm|sumTypeTerm, "->") ^^ { case p => p.reduceRight(TypeFun.apply) }
def numericRec(c:Int,s:Term): (Int,Term) = {
if(c<=0) (0,s)
else numericRec(c-1,Succ(s))
}
def isNumeric(t: Term): Boolean = t match {
case Zero => true
case Pred(v) => isNumeric(v)
case Succ(v) => isNumeric(v)
case _ => false;
}
def isReducible(t: Term): Boolean = try {
reduce(t)
true
} catch { _ =>
false
}
var count: Int = 0
def alpha(t: Abs): Abs = t match {
case Abs(x,tp, t) =>
val uid = x.toString()+"_"+count
count += 1
Abs(uid, tp, recurseUID(x, t, uid))
}
def recurseUID(x: String, t: Term, uid: String): Term = t match {
case True | False | Zero => t
case Var(v) => if (v == x) Var(uid) else t
case Abs(v, tp, t) => if(v==x) Abs(uid, tp, recurseUID(x, t, uid))
else Abs(v, tp, recurseUID(x, t, uid))
case App(t1, t2) => App(recurseUID(x, t1, uid), recurseUID(x, t2, uid))
case Succ(t) => Succ(recurseUID(x, t, uid))
case Pred(t) => Pred(recurseUID(x, t, uid))
case IsZero(t) => IsZero(recurseUID(x, t,uid))
case If(cond, t1, t2) => If(recurseUID(x, cond, uid), recurseUID(x, t1, uid), recurseUID(x, t2, uid))
case TermPair(t1, t2) => TermPair(recurseUID(x, t1, uid), recurseUID(x, t2,uid))
case First(t) => First(recurseUID(x, t, uid))
case Second(t) => Second(recurseUID(x, t, uid))
case Inl(t,tp) => Inl(recurseUID(x, t, uid),tp)
case Inr(t,tp) => Inr(recurseUID(x, t, uid),tp)
case Case(t,x1,t1,x2,t2) =>
val nt1 = if (x1==x) t1 else recurseUID(x, t1, uid)
val nt2 = if (x2==x) t2 else recurseUID(x, t2, uid)
Case(recurseUID(x, t, uid),x1,nt1,x2,nt2)
case Fix(t) => Fix(recurseUID(x, t, uid))
}
def FV(t: Term): List[String] = t match {
case True | False | Zero => List.empty
case Var(v) => List(v)
case Abs(v, tp, lt) => FV(lt).filterNot(_==v)
case App(lt1, lt2) => FV(lt1):::FV(lt2)
case Succ(t) => FV(t)
case Pred(t) => FV(t)
case IsZero(t) => FV(t)
case If(cond,t1,t2) => FV(cond):::FV(t1):::FV(t2)
case TermPair(t1,t2) => FV(t1):::FV(t2)
case First(t) => FV(t)
case Second(t) => FV(t)
case Inl(t,_) => FV(t)
case Inr(t,_) => FV(t)
case Case(t,x1,t1,x2,t2) => FV(t)++: (FV(t1) diff List(x1)) ++: (FV(t2) diff List(x1))
case Fix(t) => FV(t)
}
def subst(t: Term, x: String, s: Term): Term = t match {
case True | False | Zero => t
case Var(v) if (v==x) => s
case Var(_) => t
case If(t1, t2, t3) => If(subst(t1,x,s), subst(t2,x,s), subst(t3,x,s))
case Pred(t) => Pred(subst(t,x,s))
case Succ(t) => Succ(subst(t,x,s))
case IsZero(t) => IsZero(subst(t,x,s))
case t@Abs(v, tp, lt) => if (v==x) t
else if (FV(s).contains(v)) subst(alpha(t),x,s)
else Abs(v,tp,subst(lt,x,s))
case App(t1, t2) => App(subst(t1,x,s), subst(t2,x,s))
case TermPair(t1, t2) => TermPair(subst(t1,x,s), subst(t2,x,s))
case First(t) => First(subst(t,x,s))
case Second(t) => Second(subst(t,x,s))
case Inl(t,tp) => Inl(subst(t,x,s),tp)
case Inr(t,tp) => Inr(subst(t,x,s),tp)
case Case(t,x1,t1,x2,t2) => {
var nt1 = if(x1==x) t1 else subst(t1,x,s);
var nt2 = if(x2==x) t2 else subst(t2,x,s);
Case(subst(t,x,s),x1, nt1, x2, nt2);
}
case Fix(t) => Fix(subst(t,x,s))
}
/** Call by value reducer. */
def reduce(t: Term): Term = t match {
case If(True, t1, t2) => t1
case If(False, t1, t2) => t2
case If(cond, t1, t2) => If(reduce(cond), t1, t2)
case IsZero(Zero) => True
case IsZero(Succ(v)) if isNumeric(v) => False
case IsZero(t) => IsZero(reduce(t))
case Pred(Zero) => Zero
case Pred(Succ(v)) if isNumeric(v) => v
case Pred(v) => Pred(reduce(v))
case Succ(t) => Succ(reduce(t))
case App(t1,t2) if isReducible(t1) => App(reduce(t1),t2)
case App(t1,t2) if isReducible(t2) => App(t1,reduce(t2))
case App(Abs(x, tp, t1), t2) => subst(t1,x,t2)
case First(TermPair(t,_)) if !isReducible(t) => t
case First(t) => First(reduce(t))
case Second(TermPair(_,t)) if !isReducible(t) => t
case Second(t) => Second(reduce(t))
case TermPair(t1,t2) if isReducible(t1) => TermPair(reduce(t1), t2)
case TermPair(t1,t2) if isReducible(t2) => TermPair(t1, reduce(t2))
case Inl(t,v) => Inl(reduce(t),v)
case Inr(t,v) => Inr(reduce(t),v)
case Case(tt,x1,t1,x2,t2) if !isReducible(tt) => tt match {
case Inl(v,_) => subst(t1,x1,v)
case Inr(v,_) => subst(t2,x2,v)
case _ => throw NoRuleApplies(t)
}
case Case(t,x1,t1,x2,t2) => Case(reduce(t),x1,t1,x2,t2)
case Fix(Abs(x,_,t1)) => subst(t1,x,t)
case Fix(t) => Fix(reduce(t))
case _ => throw NoRuleApplies(t)
}
/** Thrown when no reduction rule applies to the given term. */
case class NoRuleApplies(t: Term) extends Exception(t.toString)
/** Print an error message, together with the position where it occured. */
case class TypeError(t: Term, msg: String) extends Exception(msg) {
override def toString = msg + "\n" + t
}
/** The context is a list of variable names paired with their type. */
type Context = List[(String, Type)]
/** Returns the type of the given term <code>t</code>.
*
* @param ctx the initial context
* @param t the given term
* @return the computed type
*/
def typeof(ctx: Context, term: Term): Type =
term match {
case True | False => TypeBool
case Zero => TypeNat
case Succ(t) => {
val innerType = typeof(ctx, t)
if(innerType == TypeNat) TypeNat
else throw TypeError(term, f"Not typeable, Should be Nat, is $innerType")
}
case Pred(t) => {
val innerType = typeof(ctx, t)
if(innerType == TypeNat) TypeNat
else throw TypeError(term, f"Not typeable, Should be Nat, is $innerType")
}
case IsZero(t) => {
val innerType = typeof(ctx, t)
if(innerType == TypeNat) TypeBool
else throw TypeError(term, f"Not typeable, Should be Nat, is $innerType")
}
case If(cond, t1, t2) => {
val condType = typeof(ctx, cond)
val ifType = typeof(ctx, t1)
val elseType = typeof(ctx, t2)
if(condType == TypeBool && ifType == elseType) ifType
else throw TypeError(term, f"Not typeable, If not consistent: $condType $ifType, $elseType")
}
case Var(name) => ctx.find { _._1 == name } match {
case Some(_, t) => t
case None => throw TypeError(term, f"Not typeable, $name not found")
}
case TermPair(t1, t2) => TypePair(typeof(ctx, t1), typeof(ctx, t2))
case First(t) => typeof(ctx,t) match {
case TypePair(t1, t2) => t1
case _ => throw TypeError(term, f"Not typeable. Should be Pair, found ${typeof(ctx, t)}")
}
case Second(t) => typeof(ctx,t) match {
case TypePair(t1, t2) => t2
case _ => throw TypeError(term, f"Not typeable. Should be Pair, found ${typeof(ctx, t)}")
}
case App(t1, t2) => {
val t2type = typeof(ctx, t2)
val t1type = typeof(ctx, t1)
t1type match {
case TypeFun(funt1, funt2) if funt1 == t2type => funt2
case TypeFun(funt1, funt2) => throw TypeError(term, f"Not typeable. Should be $funt1, found $t2type")
case _ => throw TypeError(term, f"Not typeable. Should be Fun, found $t1type")
}
}
case Abs(x, t1, t) => TypeFun(t1, typeof(ctx.appended((x, t1)), t))
case Inl(t, tpe) => tpe match {
case TypeSum(tp, _) if tp == typeof(ctx, t) => tpe
case _ => throw TypeError(term, f"Not typeable. Should be Sum, found ${typeof(ctx, t)}")
}
case Inr(t, tpe) => tpe match {
case TypeSum(_, tp) if tp == typeof(ctx, t) => tpe
case _ => throw TypeError(term, f"Not typeable. Should be Sum, found ${typeof(ctx, t)}")
}
case Case(t, x1, t1, x2, t2) => typeof(ctx, t) match {
case TypeSum(a1, a2) =>
val ntp1 = typeof(ctx.appended((x1, a1)), t1)
val ntp2 = typeof(ctx.appended((x2, a2)), t2)
if (ntp1 == ntp2) ntp1
else throw new TypeError(term, f"Not typeable. Should be Sum, found ${typeof(ctx, t)}")
case _ => throw new TypeError(term, f"Not typeable. Should be Sum, found ${typeof(ctx, t)}")
}
case Fix(t) => typeof(ctx, t) match {
case TypeFun(tp1, tp2) if tp1==tp2 => tp2
case _ => throw new TypeError(term, f"Not typeable. Should be Fun, found ${typeof(ctx, t)}")
}
case null => throw TypeError(term, f"Unexpected Type: ${typeof(ctx,term)}")
}
def typeof(t: Term): Type = try {
typeof(Nil, t)
} catch {
case err @ TypeError(_, _) =>
Console.println(err)
null
}
/** Returns a stream of terms, each being one step of reduction.
*
* @param t the initial term
* @param reduce the evaluation strategy used for reduction.
* @return the stream of terms representing the big reduction.
*/
def path(t: Term, reduce: Term => Term): LazyList[Term] =
try {
var t1 = reduce(t)
LazyList.cons(t, path(t1, reduce))
} catch {
case NoRuleApplies(_) =>
LazyList.cons(t, LazyList.empty)
}
def main(args: Array[String]): Unit = {
val stdin = new java.io.BufferedReader(new java.io.InputStreamReader(System.in))
val tokens = new lexical.Scanner(stdin.readLine())
phrase(term)(tokens) match {
case Success(trees, _) =>
try {
println("parsed: " + trees)
println("typed: " + typeof(Nil, trees))
for (t <- path(trees, reduce))
println(t)
} catch {
case tperror: Exception => println(tperror.toString)
}
case e =>
println(e)
}
}
}

View File

@@ -0,0 +1,84 @@
package fos
import scala.util.parsing.input.Positional
/** Abstract Syntax Trees for terms. */
sealed abstract class Term extends Positional
case object True extends Term {
override def toString() = "true"
}
case object False extends Term {
override def toString() = "false"
}
case object Zero extends Term {
override def toString() = "0"
}
case class Succ(t: Term) extends Term {
override def toString() = s"(succ $t)"
}
case class Pred(t: Term) extends Term {
override def toString() = s"(pred $t)"
}
case class IsZero(t: Term) extends Term {
override def toString() = s"(iszero $t)"
}
case class If(cond: Term, t1: Term, t2: Term) extends Term {
override def toString() = s"if $cond then $t1 else $t2"
}
case class Var(name: String) extends Term {
override def toString() = name
}
case class Abs(v: String, tp: Type, t: Term) extends Term {
override def toString() = s"(\\$v: $tp. $t)"
}
case class App(t1: Term, t2: Term) extends Term {
override def toString() = s"($t1 $t2)"
}
case class TermPair(t1: Term, t2: Term) extends Term {
override def toString() = s"{ $t1, $t2 }"
}
case class First(t: Term) extends Term {
override def toString() = s"(fst $t)"
}
case class Second(t: Term) extends Term {
override def toString() = s"(snd $t)"
}
case class Inl(t: Term, tpe: Type) extends Term {
override def toString() = s"(inl $t as $tpe)"
}
case class Inr(t: Term, tpe: Type) extends Term {
override def toString() = s"(inr $t as $tpe)"
}
case class Case(t: Term, x1: String, t1: Term, x2: String, t2: Term) extends Term {
override def toString() = s"(case $t of inl $x1 => $t1 | inr $x2 => $t2)"
}
case class Fix(t: Term) extends Term {
override def toString() = s"(fix $t)"
}
/** Abstract Syntax Trees for types. */
abstract class Type extends Positional
case object TypeBool extends Type {
override def toString() = "Bool"
}
case object TypeNat extends Type {
override def toString() = "Nat"
}
case class TypeFun(t1: Type, t2: Type) extends Type {
override def toString() = s"($t1 -> $t2)"
}
case class TypePair(t1: Type, t2: Type) extends Type {
override def toString() = s"($t1 * $t2)"
}
case class TypeSum(t1: Type, t2: Type) extends Type {
override def toString() = s"($t1 + $t2)"
}

View File

@@ -0,0 +1,280 @@
#!/usr/bin/env python3
# This script was contributed by Timothée Loyck Andres.
import argparse
import json
import os.path
import pathlib
import shutil
import ssl
import tempfile
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from getpass import getpass
from smtplib import SMTP_SSL, SMTPAuthenticationError, SMTPDataError
from typing import List, Optional, Any, Union
# ========================= CONFIG VARIABLES =========================
SMTP_SERVER = 'mail.epfl.ch'
BOT_EMAIL_ADDRESS = 'lamp.fos.bot@gmail.com'
# The minimum number of people in a group
MIN_SCIPERS = 1
# The maximum number of people in a group
MAX_SCIPERS = 3
# The number of the project (set to None to prompt user)
PROJECT_NUMBER = None
# The name of the file in which the submission configuration is stored
CONFIG_FILE_NAME = '.submission_info.json'
# The name of the folder to be zipped
SRC_FOLDER = 'src'
# ====================================================================
project_path = pathlib.Path(__file__).parent.resolve()
config_path = f'{project_path}/{CONFIG_FILE_NAME}'
src_path = f'{project_path}/{SRC_FOLDER}'
tmp_folder = tempfile.gettempdir()
class ConfigData:
def __init__(self, data_values: Optional[dict] = None, **kwargs):
"""
Creates a new data object with the given values. A dictionary may be passed, or keyword arguments for each data
piece.
Contains: email address, username, SCIPER numbers of the group's members, project number
:argument data_values: an optional dictionary containing the required data
:keyword email: the email of the user
:keyword username: the GASPAR id of the user
:keyword scipers: the list of SCIPER numbers of the group's members
:keyword project_num: the project's number
"""
if data_values is not None:
data = data_values
elif len(kwargs) > 0:
data = kwargs
else:
data = dict()
self.email: str = data.get('email')
self.username: str = data.get('username')
self.scipers: List[str] = data.get('scipers')
self.project_num: int = data.get('project_num')
def get_config_data(self) -> dict:
return {
'email': self.email,
'username': self.username,
'scipers': self.scipers,
'project_num': self.project_num
}
def get_scipers() -> List:
"""
Retrieves the group's SCIPER numbers.
:return: a list containing the SCIPER numbers of all the members of the FoS group
"""
def is_sciper(string: str) -> bool:
try:
return len(string) == 6 and int(string) > 0
except TypeError:
return False
num_scipers = None
scipers: List[str] = []
while num_scipers is None:
num_scipers = get_sanitized_input(
"Number of people in the group: ",
int,
predicate=lambda n: MIN_SCIPERS <= n <= MAX_SCIPERS,
predicate_error_msg=f"The number of people must be between {MIN_SCIPERS} and {MAX_SCIPERS} included."
)
for i in range(num_scipers):
sciper = None
while sciper is None:
sciper = get_sanitized_input(
f"SCIPER {i + 1}: ",
predicate=is_sciper,
predicate_error_msg="Invalid SCIPER number. Please try again."
)
scipers.append(sciper)
return scipers
def get_sanitized_input(prompt: str, value_type: type = str, **kwargs) -> Optional[Any]:
"""
Sanitizes the user's input.
:param prompt: the message to be displayed for the user
:param value_type: the type of value that we expect, for example str or int
:keyword allow_empty: allow the input to be empty. The returned string may be the empty string
:keyword predicate: a function that, when applied to the sanitized input, checks if it is valid
:keyword predicate_error_msg: a message to be displayed if the predicate returns false on the input
:return: the input as the passed type, or None if the input contained only whitespaces or if the type cast failed
"""
str_value = input(prompt).strip()
if len(str_value) > 0 or kwargs.get('allow_empty'):
try:
value = value_type(str_value)
p = kwargs.get('predicate')
if p is not None and not p(value):
if kwargs.get('predicate_error_msg') is None:
print("Invalid value. Please try again.")
elif len(kwargs.get('predicate_error_msg')) > 0:
print(kwargs.get('predicate_error_msg'))
return None
return value
except TypeError:
raise TypeError(f"Incorrect value type: {value_type}")
except ValueError:
print(f"The value could not be interpreted as type {value_type.__name__}. Please try again.")
return None
def get_config(from_file: bool = True) -> ConfigData:
"""
Retrieves the configuration for sending the email. It may be fetched from a configuration file, or if it does not
exist or is incomplete, it will ask the user for the data, then write it to the config file.
:param from_file: whether to retrieve the configuration from the config file if it exists. Default is True
:return: the configuration to use for the email
"""
data = ConfigData()
# Set project number if it is already specified
data.project_num = PROJECT_NUMBER
if from_file and not os.path.isfile(config_path):
print('Please provide data that will be used to submit your project.')
print(f'This information (sans the password) will be saved in: ./{CONFIG_FILE_NAME}')
if from_file and os.path.isfile(config_path):
with open(config_path, 'r') as config_file:
config = json.load(config_file)
if type(config) is dict:
data = ConfigData(config)
if data.scipers is None:
data.scipers = get_scipers()
while data.email is None:
data.email = get_sanitized_input("Email address: ", predicate=lambda address: '@' in address)
while data.username is None:
data.username = get_sanitized_input("Gaspar ID: ")
while data.project_num is None:
data.project_num = get_sanitized_input("Project number: ", int, predicate=lambda n: n > 0)
set_config(data)
return data
def set_config(data: ConfigData) -> None:
"""
Saves the configuration in the config file.
:param data: the data to be saved
"""
with open(config_path, 'w') as config_file:
json.dump(data.get_config_data(), config_file)
def create_email(frm: str, to: str, subject: str, content: Optional[str] = None,
attachments: Optional[Union[str, List[str]]] = None) -> MIMEMultipart:
"""
Creates an email.
:param frm: the address from which the email is sent
:param to: the address to which send the email
:param subject: the subject of the email
:param content: the content of the email. Can be empty
:param attachments: the attachments of the email. Can be a path or a list of paths
"""
message = MIMEMultipart()
message['From'] = frm
message['To'] = to
message['Subject'] = subject
if content is not None:
# Add content into body of message
message.attach(MIMEText(content, 'plain'))
if attachments is not None:
if type(attachments) is str:
attachments = [attachments]
for attachment_path in attachments:
part = MIMEBase("application", "octet-stream")
with open(attachment_path, 'rb') as attachment:
part.set_payload(attachment.read())
encoders.encode_base64(part)
part.add_header("Content-Disposition", f"attachment; filename={os.path.basename(attachment_path)}")
message.attach(part)
return message
if __name__ == '__main__':
arg_parser = argparse.ArgumentParser(description="Submits the project to the bot for grading.", allow_abbrev=False)
arg_parser.add_argument('-r', '--reset', action='store_true', help="ask for the submission data even if previously "
"specified")
arg_parser.add_argument('-s', '--self', action='store_true', help="send the mail to yourself instead of the bot")
if not os.path.isdir(src_path):
arg_parser.exit(1, f"No {SRC_FOLDER} folder found. Aborting.\n")
args = arg_parser.parse_args()
config: ConfigData = get_config(from_file=not args.reset)
password: str = getpass("Gaspar password: ")
recipient = config.email if args.self else BOT_EMAIL_ADDRESS
mail = create_email(
config.email,
recipient,
f"Project {config.project_num} ({', '.join(config.scipers)})",
attachments=shutil.make_archive(f'{tmp_folder}/{SRC_FOLDER}', 'zip', root_dir=project_path,
base_dir=f'{SRC_FOLDER}')
)
with SMTP_SSL(SMTP_SERVER, context=ssl.create_default_context()) as server:
try:
server.login(config.username, password)
server.sendmail(config.email, recipient, mail.as_string())
print(f"Submission sent to {recipient}.")
except SMTPAuthenticationError as e:
if e.smtp_code == 535:
print(f"Wrong GASPAR ID ({config.username}) or password. Your ID will be asked for again on the next"
" run.")
# Remove (potentially) incorrect ID from config
config.username = None
set_config(config)
exit(2)
else:
raise
except SMTPDataError as e:
if e.smtp_code == 550:
print("You email address seems to be incorrect. It will be asked for again on the next run.")
# Remove incorrect address from config
config.email = None
set_config(config)
exit(2)
else:
raise

View File

@@ -0,0 +1,4 @@
scalaVersion := "3.0.2"
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"

Binary file not shown.

View File

@@ -0,0 +1 @@
sbt.version=1.5.5

View File

@@ -0,0 +1,131 @@
package fos
object Infer {
case class TypeScheme(params: List[TypeVar], tp: Type)
type Env = List[(String, TypeScheme)]
type Constraint = (Type, Type)
case class TypeError(msg: String) extends Exception(msg)
extension (t: Type)
def in(s: Type): Boolean = getTV(s).contains(t)
extension (e: Env)
def has(t: TypeVar): Boolean = e.find(_._2.params.contains(t)).nonEmpty
private var ftv_count = -1
def freshTV(): TypeVar = {
ftv_count += 1
TypeVar("v"+ftv_count.toString)
}
def getTV(t: Type): List[TypeVar] = getTV(t, Nil)
def getTV(t: Type, ex: List[TypeVar]): List[TypeVar] = t match {
case tpe@TypeVar(_) if !(ex.contains(tpe))=> List(tpe)
case FunType(tpe1, tpe2) => getTV(tpe1,ex) ::: getTV(tpe2,ex)
case _ => Nil
}
def generalizeTV(tp: Type, env: Env) : TypeScheme = TypeScheme(getTV(tp).filter(env.has), tp)
def collectHelper(env: Env, t: Term, tpe: Type): (Type, List[Constraint]) =
collectHelper(env, t, tpe, tpe)
def collectHelper(env: Env, t: Term, tpe1: Type, tpe2: Type): (Type, List[Constraint]) =
val (tp1, c1) = collect(env,t)
(tpe1, (tp1, tpe2) :: c1)
def collectHelperAbs(env: Env, x: String, t: Term, tpe: Type): (Type, List[Constraint]) =
val (tp1, c1) = collect((x, TypeScheme(Nil,tpe)) :: env, t)
(FunType(tpe, tp1), c1)
def collectHelperIf(env: Env, t1: Term, t2: Term, t3: Term): (Type, List[Constraint]) =
val (tp1, c1) = collect(env, t1)
val (tp2, c2) = collect(env, t2)
val (tp3, c3) = collect(env, t3)
(tp2, (tp1, BoolType) :: (tp2, tp3) :: c1 ::: c2 ::: c3)
def collectHelperApp(env: Env, t1: Term, t2: Term, tpe: Type): (Type, List[Constraint]) =
val (tp1, c1) = collect(env, t1)
val (tp2, c2) = collect(env, t2)
(tpe, (tp1, FunType(tp2, tpe)) :: c1 ::: c2)
def collectHelperLet(env: Env, x: String, t1: Term, t2: Term): (Type, List[Constraint]) =
val (tp1, c1) = collect(env, t1)
val subst = unify(c1)
val tp1Unified = subst(tp1)
val fenv = env
.map((s,ts) => (s, ts.params, subst(ts.tp)))
.map((s,tsp,sub) => (s, TypeScheme(tsp.filter(_.in(sub)), sub)))
val ts = generalizeTV(tp1Unified, fenv)
val (tp2, c2) = collect((x, ts) :: fenv, t2)
(tp2, c1 ::: c2)
def collect(env: Env, t: Term): (Type, List[Constraint]) =
// println("Term: "+t)
// println("Env: "+env)
t match {
case True | False => (BoolType, Nil)
case Zero => (NatType, Nil)
case Pred(t1) => collectHelper(env, t1, NatType)
case Succ(t1) => collectHelper(env, t1, NatType)
case IsZero(t1) => collectHelper(env, t1, BoolType, NatType)
case If(t1, t2, t3) => collectHelperIf(env, t1, t2, t3)
case Var(x) if (env.exists(_._1 == x)) => ( env.find(_._1 == x).get._2.tp, Nil)
case Abs(x, tp, t1) => tp match {
case EmptyTypeTree() => collectHelperAbs(env, x, t1, freshTV())
case _ => collectHelperAbs(env, x, t1, tp.tpe)
}
case App(t1, t2) => collectHelperApp(env, t1, t2, freshTV())
case Let(x, tp, t1, t2) => tp match {
case EmptyTypeTree() => collectHelperLet(env, x, t1, t2)
case _ => collect(env, App(Abs(x, tp, t2), t1))
}
case _ => throw TypeError(f"Collect is stuck on term `$t` !")
}
def unify(c: List[Constraint]): Type => Type = {
val substitutions = unifyRec(c, Map())
// println("Subst: "+ substitutions)
return (x) => subst(x,substitutions)
}
def subst(tpe: Type, constraints: Map[Type, Type]): Type = tpe match {
case FunType(tpe1, tpe2) => FunType(subst(tpe1, constraints), subst(tpe2, constraints))
case a@TypeVar(_) if constraints.contains(a) => subst(constraints(a), constraints)
case others => others
}
def unifyRec(c: List[Constraint], substitutions: Map[Type, Type]): Map[Type, Type] = {
if (c.isEmpty)
return substitutions
// println("c: "+ c)
// println("Subst: "+ substitutions)
c.head match {
case (s, t) if s == t => unifyRec(c.tail, substitutions)
case (s@TypeVar(_), t) if !(s in t) => unifyRec(applyMapping(c.tail, s -> t), substitutions + (s -> t))
case (s, t@TypeVar(_)) if !(t in s) => unifyRec(applyMapping(c.tail, t -> s), substitutions + (t -> s))
case (FunType(s1, s2), FunType(t1, t2)) => unifyRec((s1, t1) :: (s2, t2) :: c.tail, substitutions)
case _ => throw new TypeError(f"Impossible to find a substitution that satisfies the constraint set `${c.head}`")
}
}
def applyMapping(constraints: List[Constraint], st: (TypeVar, Type)): List[Constraint] =
val (s, t) = st
def recMapping(tp: Type): Type = tp match {
case tp if tp == s => t
case FunType(a, b) => FunType(recMapping(a), recMapping(b))
case tp => tp
}
constraints.map {
case (`s`, `s`) => (t, t)
case (`s`, y) => (t, y)
case (x , `s`) => (x, t)
case (x, y) => (recMapping(x), recMapping(y))
}
}

View File

@@ -0,0 +1,25 @@
package fos
import Parser._
import scala.util.parsing.input._
object Launcher {
def main(args: Array[String]) = {
val stdin = new java.io.BufferedReader(new java.io.InputStreamReader(System.in))
val tokens = new lexical.Scanner(stdin.readLine())
phrase(term)(tokens) match {
case Success(term, _) =>
try {
val (tpe, c) = Infer.collect(Nil, term)
// println("TPE: "+tpe)
// println("C: "+c)
val sub = Infer.unify(c)
println("typed: " + sub(tpe))
} catch {
case tperror: Exception => println("type error: " + tperror.getMessage)
}
case e =>
println(e)
}
}
}

View File

@@ -0,0 +1,74 @@
package fos
import scala.util.parsing.combinator.syntactical.StandardTokenParsers
import scala.util.parsing.input._
object Parser extends StandardTokenParsers {
lexical.delimiters ++= List("(", ")", "\\", ".", ":", "=", "->", "{", "}", ",", "*", "+")
lexical.reserved ++= List("Bool", "Nat", "true", "false", "if", "then", "else", "succ",
"pred", "iszero", "let", "in")
/** <pre>
* Term ::= SimpleTerm { SimpleTerm }</pre>
*/
def term: Parser[Term] = positioned(
simpleTerm ~ rep(simpleTerm) ^^ { case t ~ ts => (t :: ts).reduceLeft[Term](App.apply) }
| failure("illegal start of term"))
/** <pre>
* SimpleTerm ::= "true"
* | "false"
* | number
* | "succ" Term
* | "pred" Term
* | "iszero" Term
* | "if" Term "then" Term "else" Term
* | ident
* | "\" ident [":" Type] "." Term
* | "(" Term ")"
* | "let" ident [":" Type] "=" Term "in" Term</pre>
*/
def simpleTerm: Parser[Term] = positioned(
"true" ^^^ True
| "false" ^^^ False
| numericLit ^^ { case chars => lit2Num(chars.toInt) }
| "succ" ~ term ^^ { case "succ" ~ t => Succ(t) }
| "pred" ~ term ^^ { case "pred" ~ t => Pred(t) }
| "iszero" ~ term ^^ { case "iszero" ~ t => IsZero(t) }
| "if" ~ term ~ "then" ~ term ~ "else" ~ term ^^ {
case "if" ~ t1 ~ "then" ~ t2 ~ "else" ~ t3 => If(t1, t2, t3)
}
| ident ^^ { case id => Var(id) }
| "\\" ~ ident ~ opt(":" ~ typ) ~ "." ~ term ^^ {
case "\\" ~ x ~ Some(":" ~ tp) ~ "." ~ t => Abs(x, tp, t)
case "\\" ~ x ~ None ~ "." ~ t => Abs(x, EmptyTypeTree(), t)
}
| "(" ~> term <~ ")" ^^ { case t => t }
| "let" ~ ident ~ opt(":" ~ typ) ~ "=" ~ term ~ "in" ~ term ^^ {
case "let" ~ x ~ Some(":" ~ tp) ~ "=" ~ t1 ~ "in" ~ t2 => Let(x, tp, t1, t2)
case "let" ~ x ~ None ~ "=" ~ t1 ~ "in" ~ t2 => Let(x, EmptyTypeTree(), t1, t2)
}
| failure("illegal start of simple term"))
/** <pre>
* Type ::= SimpleType { "->" Type }</pre>
*/
def typ: Parser[TypeTree] = positioned(
baseType ~ opt("->" ~ typ) ^^ {
case t1 ~ Some("->" ~ t2) => FunTypeTree(t1, t2)
case t1 ~ None => t1
}
| failure("illegal start of type"))
/** <pre>
* BaseType ::= "Bool" | "Nat" | "(" Type ")"</pre>
*/
def baseType: Parser[TypeTree] = positioned(
"Bool" ^^^ BoolTypeTree()
| "Nat" ^^^ NatTypeTree()
| "(" ~> typ <~ ")" ^^ { case t => t }
)
private def lit2Num(n: Int): Term =
if (n == 0) Zero else Succ(lit2Num(n - 1))
}

View File

@@ -0,0 +1,83 @@
package fos
import scala.util.parsing.input.Positional
sealed abstract class Term extends Positional
case object True extends Term {
override def toString() = "true"
}
case object False extends Term {
override def toString() = "false"
}
case object Zero extends Term {
override def toString() = "0"
}
case class Succ(t: Term) extends Term {
override def toString() = "succ " + t
}
case class Pred(t: Term) extends Term {
override def toString() = "pred " + t
}
case class IsZero(t: Term) extends Term {
override def toString() = "iszero " + t
}
case class If(cond: Term, t1: Term, t2: Term) extends Term {
override def toString() = "if " + cond + " then " + t1 + " else " + t2
}
case class Var(name: String) extends Term {
override def toString() = name
}
case class Abs(v: String, tp: TypeTree, t: Term) extends Term {
override def toString() = "(\\" + v + ":" + tp + "." + t + ")"
}
case class App(t1: Term, t2: Term) extends Term {
override def toString() = t1.toString + (t2 match {
case App(_, _) => " (" + t2.toString + ")" // left-associative
case _ => " " + t2.toString
})
}
case class Let(x: String, tp: TypeTree, v: Term, t: Term) extends Term {
override def toString() = "let " + x + ":" + tp + " = " + v + " in " + t
}
// Note that TypeTree is distinct from Type.
// The former is how types are parsed, the latter is how types are represented.
// We need this distinction because:
// 1) There are type vars, which can't be written by our users, but are needed by the inferencer.
// 2) There are empty types, which can be written, but aren't directly supported by the inferencer.
abstract class TypeTree extends Positional {
def tpe: Type
}
case class BoolTypeTree() extends TypeTree {
override def tpe = BoolType
override def toString() = "Bool"
}
case class NatTypeTree() extends TypeTree {
override def tpe = NatType
override def toString() = "Nat"
}
case class FunTypeTree(t1: TypeTree, t2: TypeTree) extends TypeTree {
override def tpe = FunType(t1.tpe, t2.tpe)
override def toString() = (t1 match {
case FunTypeTree(_, _) => "(" + t1 + ")" // right-associative
case _ => t1.toString
}) + "->" + t2
}
case class EmptyTypeTree() extends TypeTree {
override def tpe = throw new UnsupportedOperationException
override def toString() = "_"
}

View File

@@ -0,0 +1,22 @@
package fos
// Note that TypeTree is distinct from Type.
// See a comment on TypeTree to learn more.
abstract class Type
case class TypeVar(name: String) extends Type {
override def toString() = name
}
case class FunType(t1: Type, t2: Type) extends Type {
override def toString() = "(" + t1 + " -> " + t2 + ")"
}
case object NatType extends Type {
override def toString() = "Nat"
}
case object BoolType extends Type {
override def toString() = "Bool"
}

View File

@@ -0,0 +1,280 @@
#!/usr/bin/env python3
# This script was contributed by Timothée Loyck Andres.
import argparse
import json
import os.path
import pathlib
import shutil
import ssl
import tempfile
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from getpass import getpass
from smtplib import SMTP_SSL, SMTPAuthenticationError, SMTPDataError
from typing import List, Optional, Any, Union
# ========================= CONFIG VARIABLES =========================
SMTP_SERVER = 'mail.epfl.ch'
BOT_EMAIL_ADDRESS = 'lamp.fos.bot@gmail.com'
# The minimum number of people in a group
MIN_SCIPERS = 1
# The maximum number of people in a group
MAX_SCIPERS = 3
# The number of the project (set to None to prompt user)
PROJECT_NUMBER = None
# The name of the file in which the submission configuration is stored
CONFIG_FILE_NAME = '.submission_info.json'
# The name of the folder to be zipped
SRC_FOLDER = 'src'
# ====================================================================
project_path = pathlib.Path(__file__).parent.resolve()
config_path = f'{project_path}/{CONFIG_FILE_NAME}'
src_path = f'{project_path}/{SRC_FOLDER}'
tmp_folder = tempfile.gettempdir()
class ConfigData:
def __init__(self, data_values: Optional[dict] = None, **kwargs):
"""
Creates a new data object with the given values. A dictionary may be passed, or keyword arguments for each data
piece.
Contains: email address, username, SCIPER numbers of the group's members, project number
:argument data_values: an optional dictionary containing the required data
:keyword email: the email of the user
:keyword username: the GASPAR id of the user
:keyword scipers: the list of SCIPER numbers of the group's members
:keyword project_num: the project's number
"""
if data_values is not None:
data = data_values
elif len(kwargs) > 0:
data = kwargs
else:
data = dict()
self.email: str = data.get('email')
self.username: str = data.get('username')
self.scipers: List[str] = data.get('scipers')
self.project_num: int = data.get('project_num')
def get_config_data(self) -> dict:
return {
'email': self.email,
'username': self.username,
'scipers': self.scipers,
'project_num': self.project_num
}
def get_scipers() -> List:
"""
Retrieves the group's SCIPER numbers.
:return: a list containing the SCIPER numbers of all the members of the FoS group
"""
def is_sciper(string: str) -> bool:
try:
return len(string) == 6 and int(string) > 0
except TypeError:
return False
num_scipers = None
scipers: List[str] = []
while num_scipers is None:
num_scipers = get_sanitized_input(
"Number of people in the group: ",
int,
predicate=lambda n: MIN_SCIPERS <= n <= MAX_SCIPERS,
predicate_error_msg=f"The number of people must be between {MIN_SCIPERS} and {MAX_SCIPERS} included."
)
for i in range(num_scipers):
sciper = None
while sciper is None:
sciper = get_sanitized_input(
f"SCIPER {i + 1}: ",
predicate=is_sciper,
predicate_error_msg="Invalid SCIPER number. Please try again."
)
scipers.append(sciper)
return scipers
def get_sanitized_input(prompt: str, value_type: type = str, **kwargs) -> Optional[Any]:
"""
Sanitizes the user's input.
:param prompt: the message to be displayed for the user
:param value_type: the type of value that we expect, for example str or int
:keyword allow_empty: allow the input to be empty. The returned string may be the empty string
:keyword predicate: a function that, when applied to the sanitized input, checks if it is valid
:keyword predicate_error_msg: a message to be displayed if the predicate returns false on the input
:return: the input as the passed type, or None if the input contained only whitespaces or if the type cast failed
"""
str_value = input(prompt).strip()
if len(str_value) > 0 or kwargs.get('allow_empty'):
try:
value = value_type(str_value)
p = kwargs.get('predicate')
if p is not None and not p(value):
if kwargs.get('predicate_error_msg') is None:
print("Invalid value. Please try again.")
elif len(kwargs.get('predicate_error_msg')) > 0:
print(kwargs.get('predicate_error_msg'))
return None
return value
except TypeError:
raise TypeError(f"Incorrect value type: {value_type}")
except ValueError:
print(f"The value could not be interpreted as type {value_type.__name__}. Please try again.")
return None
def get_config(from_file: bool = True) -> ConfigData:
"""
Retrieves the configuration for sending the email. It may be fetched from a configuration file, or if it does not
exist or is incomplete, it will ask the user for the data, then write it to the config file.
:param from_file: whether to retrieve the configuration from the config file if it exists. Default is True
:return: the configuration to use for the email
"""
data = ConfigData()
# Set project number if it is already specified
data.project_num = PROJECT_NUMBER
if from_file and not os.path.isfile(config_path):
print('Please provide data that will be used to submit your project.')
print(f'This information (sans the password) will be saved in: ./{CONFIG_FILE_NAME}')
if from_file and os.path.isfile(config_path):
with open(config_path, 'r') as config_file:
config = json.load(config_file)
if type(config) is dict:
data = ConfigData(config)
if data.scipers is None:
data.scipers = get_scipers()
while data.email is None:
data.email = get_sanitized_input("Email address: ", predicate=lambda address: '@' in address)
while data.username is None:
data.username = get_sanitized_input("Gaspar ID: ")
while data.project_num is None:
data.project_num = get_sanitized_input("Project number: ", int, predicate=lambda n: n > 0)
set_config(data)
return data
def set_config(data: ConfigData) -> None:
"""
Saves the configuration in the config file.
:param data: the data to be saved
"""
with open(config_path, 'w') as config_file:
json.dump(data.get_config_data(), config_file)
def create_email(frm: str, to: str, subject: str, content: Optional[str] = None,
attachments: Optional[Union[str, List[str]]] = None) -> MIMEMultipart:
"""
Creates an email.
:param frm: the address from which the email is sent
:param to: the address to which send the email
:param subject: the subject of the email
:param content: the content of the email. Can be empty
:param attachments: the attachments of the email. Can be a path or a list of paths
"""
message = MIMEMultipart()
message['From'] = frm
message['To'] = to
message['Subject'] = subject
if content is not None:
# Add content into body of message
message.attach(MIMEText(content, 'plain'))
if attachments is not None:
if type(attachments) is str:
attachments = [attachments]
for attachment_path in attachments:
part = MIMEBase("application", "octet-stream")
with open(attachment_path, 'rb') as attachment:
part.set_payload(attachment.read())
encoders.encode_base64(part)
part.add_header("Content-Disposition", f"attachment; filename={os.path.basename(attachment_path)}")
message.attach(part)
return message
if __name__ == '__main__':
arg_parser = argparse.ArgumentParser(description="Submits the project to the bot for grading.", allow_abbrev=False)
arg_parser.add_argument('-r', '--reset', action='store_true', help="ask for the submission data even if previously "
"specified")
arg_parser.add_argument('-s', '--self', action='store_true', help="send the mail to yourself instead of the bot")
if not os.path.isdir(src_path):
arg_parser.exit(1, f"No {SRC_FOLDER} folder found. Aborting.\n")
args = arg_parser.parse_args()
config: ConfigData = get_config(from_file=not args.reset)
password: str = getpass("Gaspar password: ")
recipient = config.email if args.self else BOT_EMAIL_ADDRESS
mail = create_email(
config.email,
recipient,
f"Project {config.project_num} ({', '.join(config.scipers)})",
attachments=shutil.make_archive(f'{tmp_folder}/{SRC_FOLDER}', 'zip', root_dir=project_path,
base_dir=f'{SRC_FOLDER}')
)
with SMTP_SSL(SMTP_SERVER, context=ssl.create_default_context()) as server:
try:
server.login(config.username, password)
server.sendmail(config.email, recipient, mail.as_string())
print(f"Submission sent to {recipient}.")
except SMTPAuthenticationError as e:
if e.smtp_code == 535:
print(f"Wrong GASPAR ID ({config.username}) or password. Your ID will be asked for again on the next"
" run.")
# Remove (potentially) incorrect ID from config
config.username = None
set_config(config)
exit(2)
else:
raise
except SMTPDataError as e:
if e.smtp_code == 550:
print("You email address seems to be incorrect. It will be asked for again on the next run.")
# Remove incorrect address from config
config.email = None
set_config(config)
exit(2)
else:
raise