Disabled external gits
This commit is contained in:
Submodule cs452-fos/fos-assignments deleted from 74fc009d30
12
cs452-fos/fos-assignments/.gitignore
vendored
Normal file
12
cs452-fos/fos-assignments/.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
.bsp
|
||||
.idea/
|
||||
project/project/
|
||||
project/target/
|
||||
target/
|
||||
idea/
|
||||
project/project/
|
||||
project/target/
|
||||
target/
|
||||
.metals/
|
||||
.vscode/
|
||||
.bloop/
|
11
cs452-fos/fos-assignments/1-arithmetic/.gitignore
vendored
Normal file
11
cs452-fos/fos-assignments/1-arithmetic/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
.bsp
|
||||
.idea/
|
||||
project/project/
|
||||
project/target/
|
||||
target/ .bsp/
|
||||
.idea/
|
||||
project/project/
|
||||
project/target/
|
||||
target/
|
||||
.metals/
|
||||
.vscode/
|
4
cs452-fos/fos-assignments/1-arithmetic/build.sbt
Normal file
4
cs452-fos/fos-assignments/1-arithmetic/build.sbt
Normal file
@@ -0,0 +1,4 @@
|
||||
scalaVersion := "3.0.2"
|
||||
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"
|
||||
|
@@ -0,0 +1 @@
|
||||
sbt.version=1.5.5
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
4
cs452-fos/fos-assignments/2-untyped/build.sbt
Normal file
4
cs452-fos/fos-assignments/2-untyped/build.sbt
Normal file
@@ -0,0 +1,4 @@
|
||||
scalaVersion := "3.0.2"
|
||||
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"
|
||||
|
@@ -0,0 +1 @@
|
||||
sbt.version=1.5.5
|
BIN
cs452-fos/fos-assignments/2-untyped/project2.zip
Normal file
BIN
cs452-fos/fos-assignments/2-untyped/project2.zip
Normal file
Binary file not shown.
@@ -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
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
4
cs452-fos/fos-assignments/3-typed/build.sbt
Normal file
4
cs452-fos/fos-assignments/3-typed/build.sbt
Normal file
@@ -0,0 +1,4 @@
|
||||
scalaVersion := "3.0.2"
|
||||
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"
|
||||
|
@@ -0,0 +1 @@
|
||||
sbt.version=1.5.5
|
6
cs452-fos/fos-assignments/3-typed/project/metals.sbt
Normal file
6
cs452-fos/fos-assignments/3-typed/project/metals.sbt
Normal 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")
|
||||
|
@@ -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")
|
||||
|
BIN
cs452-fos/fos-assignments/3-typed/project3.zip
Normal file
BIN
cs452-fos/fos-assignments/3-typed/project3.zip
Normal file
Binary file not shown.
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 + ")"
|
||||
}
|
280
cs452-fos/fos-assignments/3-typed/submit.py
Normal file
280
cs452-fos/fos-assignments/3-typed/submit.py
Normal 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
|
4
cs452-fos/fos-assignments/4-extensions/build.sbt
Normal file
4
cs452-fos/fos-assignments/4-extensions/build.sbt
Normal file
@@ -0,0 +1,4 @@
|
||||
scalaVersion := "3.0.2"
|
||||
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"
|
||||
|
@@ -0,0 +1 @@
|
||||
sbt.version=1.5.5
|
BIN
cs452-fos/fos-assignments/4-extensions/project4.zip
Normal file
BIN
cs452-fos/fos-assignments/4-extensions/project4.zip
Normal file
Binary file not shown.
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)"
|
||||
}
|
280
cs452-fos/fos-assignments/4-extensions/submit.py
Normal file
280
cs452-fos/fos-assignments/4-extensions/submit.py
Normal 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
|
4
cs452-fos/fos-assignments/5-inference/build.sbt
Normal file
4
cs452-fos/fos-assignments/5-inference/build.sbt
Normal file
@@ -0,0 +1,4 @@
|
||||
scalaVersion := "3.0.2"
|
||||
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"
|
||||
|
BIN
cs452-fos/fos-assignments/5-inference/project.zip
Normal file
BIN
cs452-fos/fos-assignments/5-inference/project.zip
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
sbt.version=1.5.5
|
@@ -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))
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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))
|
||||
}
|
@@ -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() = "_"
|
||||
}
|
@@ -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"
|
||||
}
|
||||
|
280
cs452-fos/fos-assignments/5-inference/submit.py
Normal file
280
cs452-fos/fos-assignments/5-inference/submit.py
Normal 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
|
Reference in New Issue
Block a user