From 5615807951269776fbb753ba76f4ea165a82a676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Dec 2019 20:21:08 +0100 Subject: [PATCH] Import newInterpreter handout --- build.sbt | 12 + grading-tests.jar | Bin 0 -> 8339 bytes project/MOOCSettings.scala | 25 ++ project/StudentTasks.scala | 323 ++++++++++++++++++ project/build.properties | 1 + project/buildSettings.sbt | 7 + project/plugins.sbt | 2 + src/main/scala/newInterpreter/Logger.scala | 16 + .../newInterpreter/RecursiveLanguage.scala | 226 ++++++++++++ .../RecursiveLanguageSuite.scala | 126 +++++++ student.sbt | 9 + 11 files changed, 747 insertions(+) create mode 100644 build.sbt create mode 100644 grading-tests.jar create mode 100644 project/MOOCSettings.scala create mode 100644 project/StudentTasks.scala create mode 100644 project/build.properties create mode 100644 project/buildSettings.sbt create mode 100644 project/plugins.sbt create mode 100644 src/main/scala/newInterpreter/Logger.scala create mode 100644 src/main/scala/newInterpreter/RecursiveLanguage.scala create mode 100644 src/test/scala/newInterpreter/RecursiveLanguageSuite.scala create mode 100644 student.sbt diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..640177b --- /dev/null +++ b/build.sbt @@ -0,0 +1,12 @@ +course := "progfun2" +assignment := "newInterpreter" +name := course.value + "-" + assignment.value +testSuite := "newInterpreter.RecursiveLanguageSuite" + +scalaVersion := "0.19.0-RC1" +scalacOptions ++= Seq("-deprecation") +libraryDependencies ++= Seq( + "com.novocode" % "junit-interface" % "0.11" % Test +) + +testOptions in Test += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s") diff --git a/grading-tests.jar b/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..66a2067ec44f348f1f1d91e27e01fb79a4017f93 GIT binary patch literal 8339 zcmbuEWl$YTm#$&s?(XjH?yw2&7Tn!k65O5OZowtEI~&=!6Fj&}a02(7xj()$^WC{M zRkOOPtGa6Ss`d7-e%GU_00oT=0S^xkVO1Qi0`ZT)hJc1plF^i4kyn;wQ<6}YmzB}b zWL1)VpMZe)Hr<6dD2@&@8)W)HA=W^lUyenD7Z-!@AjOFQHGcjB?N8u}B*&9@4nxFb zM;oA=G*Fe!BOf_qjgqZ&HkHIx5*2-3g9lrdnd=NZD`&(w8hiM4T2S$k=Di_C*tTk` z7WLk08>nXBQ2G6V^w)H|fWpkV@cUy_PMyc2`v7Ro`GCy zV25Jxt2)pXMpXe09wSxkHSaGm;gApzs((pDgn)pD_&14Ye`PpXddoX`Sh~5mS^l-N z{hj>(eh;R9Pgb`y_jGf&^|Dknb+Yy}wYJpowDquL^)Pk!@SV~PbirEj+Gdit^;zJP zUq7U&S2A7@dljrbY9xJEH;13f@#T89EA%K|$kH)|rvFwJue}FJ(J;TY*`r@;VKA8P zNKijTXdw#b<4r(8C4;<$k$*zu9|)kKg-#KNFM#x`(@vo=ppBUAMK!KK#DxSb6ipOT1R>S_rR-T$6d~=*Y!O z9w+6yYU&E4XD8UjyTC!k_Q6QjJK!o8^dVz@9Dss32fm3cWhDR((oJ+(hP0HzbVD#u zHyr<$Ol8tDKEED}_xcjKImWu63sJLPB5;x))^&8@AJ!URUi!XrnX^Vj=Yn)W=H z1NbRDs9RUJ6z7@d-V`kxzuY7?loN?&mrIhGiU+TnRJ;Y42T+KTpF;AQp!pnGhtBj3 zgj9VZ%xTNeng>iz+h|rd!b`_lO#tT}RBzpp`pC{KLwh+QKa*~YL3~8H;zgBBJ^5nl zCfLR43lbSH)j1Q`r?;m|wLrgtOQwSOVTOd2{=-Yo%CcX)O-IqHP;|1jM1^uz*N?cKWED#UxU0FaCQ@?spn|x&GWZstwj& z8_4==_v7TX&e0Wmsq!trTGb){mh+9LL(0Z4nEu8)h!4}6|EO@bkN=3@n%_!T2u=99 z-Aec>6QSh}&2s*c#5?m2=qBL1?VK!K{j2Tpr_Y_EFrwNYo!lm?SJk*jm7_rAf`Sy@ z5k1H^O4rWKSiqf{3F)UpmCzH97R6R~(LTK2jOWCZ#OM!ji9dr+Z#@^D!HkT#nmiSt%q4C#<^s~>WoNCe>rOG!?JqHQf0u4<43LrkbHqsl3Q-n=ra9 z^pJg?HXJfF#_>)W;6{H1D}P2}8m`Uv4w{e4n|HpMwMh~hR(}{B_1&AaJDLNCX`v^~ z>9Z`D5X*IQ7msg+Nlc~W4B3kI- z?>|H1RaVf$k(BP3u=WcnCqj(#&__AHD@7^Bn0>3>=k_`&t&(5TJv7GxkjLJ-dM%WW z%q%Nb1@dw*5H@o!j)%yVopF0agemkj6zLNQ+L~`6akiZvgcWo9q`Xu6t0ohQ$UhWQ zvJ{~uP6X8i%InPjMh8aw=S^0nZLN5acAkuh$ntbQgrey+`QcClG7*X~r6E8?I{QM@4wpv$B^4gWXklmrBw#nAk zv?pG`B6VJJO4}BzDf?=TaY1sZb)*~}M;44ggy}GK_>_&p&^qzJ`vuRZJMAp*@Z{FJ zv0|RYG5V!ztzF{HxiS9z!eN8t-r15U6s_e72zpM!mHM%)LLEdGOS#V;`!dGqg!jQ4 zW(8}4~b@U;e6%gz7U`}q;V2ZzX-MQ%wte!2K%+nPwv94h!yc21^B)CVix40p^I@IGH zGG^UsBB!0|jm`nhJ53B)eu~3Aadb4FR<5Fvc>78fOKF33xk@!^?{^~Lsfg5#6OBLV zJ`a%UA_(38Ax}%XJT9IeO)l_BsfI%d+my}vcy(CR$JScyaoD_s<2P4ILb%yRWOT1u z)>DXLs77Z+-Zp0KUDpCioIF})vLnkY7QKr6BinMPK% z8Xnp~mx`+xNtoJi8B-VUJbLmB6jr)G8hIPhvM-IX!=^-^P3IWIk68ay(;cU-CWs$t z**OI6CQIof`Y$Hs(*PoaSyqk*Q4S5wDxWKO$p{KqGelc?&S2eXnMCBY_d7tjR49u* zpt>a8eE*|Ci}NDwJs1@;Q&q!U*2%ccPJ!=Q~kM z&J5w-v4=HJ^M;*L=jB<+u`m>3bB+;jlf5BHY?hMm#)@N8^L-n9>tVum*h*vJ9+I$% zX9T?OzF>oW)nI6mB}K376o*-Z`Fs?_8Z9Zabya4TJiZ?m;@B)B=6-K)|-ptTAwsP}wwl7P88A589 z2q(Ww&Pws#SOQNOPf9PXS_7CLZKdbb3V&BRc4Ux8BRfUS_x0kghKDr!JEM&B-?&V> zCWRa^EVyHnDJ|+!bs3X%9g{>xx*Aj#!Bq09%ihL}9wf;kS)BW+ zOm@OKZpXBbjHX9}`mkFQquIY-?+u+4!d{lr!Ih?SO|Kg<_cKe(Y6l+m&?~f;iQ*iv${Mc$l=CR0|1TrSY*DRD$tW zt(3w^4AUUzfJ)pxY7bfJs?6dTe(JcBL>^OgI_m3;szKwB#e-PPA(_rF2~Fb)9z{*; zYH@{(ePnG}r0;lYbM`uslcY-FpnMGw+)*2#q}uf-(rYy?l#cl$q8L%qQNhw)0g@lI zTM=_P)51;+rBgS{;?jK-m=2te-lP-z1>zHjLx5c6xNAgoVJa^12OzzS?!G9s6#Y02 z*SMJ@mRqrd2(%4%V&^~{9?41om87E-y^XD&d|hZ~^dc#xRG}!Ac?^vex{BF^1oDR5 z^Bd=fwhf2C1HtWDq?-Gpr`v=Zk$tF*@$+2hfwpA96K2fpHc8L75?|l zXU&bla|{FE8}YHFU_^>}YD7MP9oV$+2cLBjonv9-uvl<7!}u{Dk|IHzGHuq^aVQv9 z31+yKCoc%d7bir=6W!@pM=tlRQNvC_{^jvdZBYh zXdCLIfuECPyfUyr%I?Oep5{q=;=QD%*v06Mg?=XP-|WD^ZT)&_IZ!aaNG}hk(WR(S zVub$E$E&sVWgEHt^4oy%3C1CWlRB=W`DK);?@&u?DfO_79-3#&D4Zl?kIG8aD25G> z$0{SicSzAd!Q*6LV~j?Y6m~;3U%XDn$Qed_4?$8q<;B;NUBq#IRG+0!Z!v`+vytn; z<(|{jMi}RuE(>jEOJc9+7Nv%b_CXbbq%q%n?`T4#7#{8Ta+i_s@U2;4yQhy`>xz+o4kJq7_ z%Vz@*4QH}QbfHk*`T6_^ zsY5qh6O&S*tU~}o;sk^t=HkxfYvHX|)?qEdtry@n9l+9#Zy4?gjc@i4T*KtQQg+Oc zLXl-L<7B888?SPk!`)(CdI1whCN6oEOGWKOhQD1K>uk1~+_@MLKO-B*bCtrI+5V|k zE_MhtVZVdRZBIS9v1`%TIJ<8~apTMV3R@9KJ^uQx1sn|nrSG=uO=U!rmBZfw480@e zoj(bX#VoaP_~!EByn&)axEL+*s_F`=9TflqZLf2GRyJwtfv;Aje-HAa1A>d%b<^vk11YhmHMVj2P*+!P*WtNHEL9nU!VPt zxJ30XOebqZXIzo~xGua%DQCW8tDh%Mw|17%tfORUn&ZwHoz@Ojv=p zHW!?xy@MKB!@X>CWs%h9wP-dSn)=FdqTgb4>Ygb4zB$V^U(?|~p#C0Y>@!kdNTEYO z5YztOLB{`fHev2y>h9j7hv1{Rg!6s{rf#wzPelNj8Uw<@(8YNnlUdTns3kDPv18d% zGuZjFkeeEGYv*AMsx8%;>-L#@JP;?NdX+q4sC?P}1$9z5+}?=t}*AX{#u@L9QN_e z{njG*>JFPK)Zm1mw0AP%m6PJw6Xdnid~tfx+RKDvLiwuu9u5&~EuF!)AV6^+`gp7k zUuWZxOdnc12N_as!wx1KE@xRS!a1tPX4!(9e6DIB8O2=h7}y>s*3j3D%pHd3=!FVdi{xc{yzs=YvUotlV6Eu3&vFwymXh z(E|yxaV^!D!851t+V3#)S1^1U?0rxBVDwzOx`~?_rqC80dkeok4j!4y-k&FO4dW(U zS}i3)w=d4@K&)9^l;He)AH|8Sevy<@#sDB~B^pv353k%_k#3=!vrVcq(XXP1uM=Bj z82DF27Lk}cgmiVIa$E!(>@6fKu9hm&NY0C0nJ?TSEv1Kp!6K!`t~jPSy|G6D$b9WS z(;zO&BXrg3mTa+QqBU!f(gY9OF2mY=P6yi{-Q+a_F=i$^e~4E=HYvlVG`Rz%Kfb)F zOTHBROpa@8EbRxgh|t+;x9)Fv`^6ayPo*g$?~#_mI0E@vZe)y^|l|H5-e6moK_0*a1+%}}5Gc=XQ??k*Ix z%)7L1SV_}|BrGFgFe5AJrzJ=dE0ak}i%&Vi0KVRE=PsXX?}`~OBO)q0tA~;Kc)9Be zq=P5YR9BT|gZ`i18b;=}HETCixK)N2hTAnV#g#dr^>R>*mHTCK#T#_uq{BP3ulE^+ z26Q}BX44DH=c$8@f<1;T_X4Z>^~l2L{_#Iq!>@Oo!c?i>scNIeQ}q&A%V{j1 zX@Y2)w(3uGgW&RUDrQ4=NeJ)8oM^kS?JhTXf-_H|I58Lx&3C+?da&=&u(iB*5{U&# ze1exNv-DPb`8)kW^F(`?c*Am;43u$@zPOqJ-cS1#SQ8W_EFHKk$fTd5M$(GEL1cAD z^vSDzuue9LO#+TRm+3+^D#eVu2QVuHYfeA7>~Fs@dR5IKyb*mc4E0@?V7$4K;@^RTHKd;;wOZr+!78huIh(d`ZF)N#9Z&u!K0O$GwFS4`b%)t8Y*k>Bb%+ z*c>I*KIUE^)h_V)6n)6@KB*}~H9NRhDyn94``|1trJq0^MBm_D*BV^+(dZy&)=x#7>-eg$8bqYsq@ry{2<+ z5PQ1U9C)GO^T`6>DJCRjg07;3?hO>NZS#w=cEgF^3t|pf5G2CyE3~H zHEJhRX{gV-1c>*e&1piY_OZ6_3hzSVWtvNS*j_*tR>D6Fd6U;>OYA4PL7+u>t7=KH_>I+w2@uCEe_ zp=m*=xvo{~rk9K@*3-NPVP))xQ zj?Z#a{(@xIy1PYHc&tif%JjIxJQCVyAwYw_T8c)A12E|dyKGvT2{J6(Z^$X>_n-e^ zU_@of0GdKLSK0aMR|niP0j%h&?7dJs2`8xNT|)q`!5o9#l zHFG88$zb)UzUx*7<8p$2M`>XsuZVntyT24U70EXBa8|=@CB|)IVjUg-K2%Lzpp!oc z+lRHKF^{G~FUCCK(#K%+9n8VSUq()p?PZX#J1|snzr-`+hkg*r&KvAJ?FVrnAJA4N z8hypqsI``!Ml378`6tq}CpP#!q->g#C}XIq+lQJt{hodR?OICo#9QFKJ1huU^F|1k ziyc>p9hGjNJ0W^7Qhv`$5tq$`v2Q7zP_c>Iq{@=jN|(1#HdnriprOYp%Mo1_+ z>f=zo^mC(i>Cn+pL$|DLYm0A_8nNQFVa9XIDytJ2wRzlDwe>;4mWw)w)mS$)Am>qF zeqSh5!YCm($q03dc3(07f>iEjMSYdfag)zI}p6+cjE1dm)v$?n?MDP@V-&2NHyyC37^uY=Gin)E^ zlN&0%7Sm*0hy?iP=U`BlbPSh(P~^A*0jP^`$YKve!>zv7LH%3PyrLhEC^OJV`Ae&> zF@=$*eD_3rzOv<4orBv?Fu$s&+UBw{yhin5j|K2*&W3#z2?f6qEF z`07_J2rVz66?Q&`2}~h1Jq?J)OA+YwmdSN(<9*oLOxBos9V9|y>nX*><<4y6`yW62ERdXu?2b;D# zfrKYixC)D*FGd|6&JRx+xK2%SJy(?Us_W$1ODX}E1Wj>|JAZ^-guz78K?26FmvP7* z8fzuvPPeWwn-#*xRg3O5rry1Tqy`M5Ix*3oETyU&BMjU`YSji_q|FVB+?>nD?+*^j zsPg9~lChqn;wSj+?Q%9(zrnh8GoZ^&d~nVu$}*x?I-r_pXfAZdZqvtt@JE}n@|82K z%+T1Ld!lyc0oSy1+i{d85>;!gT`HQ+wb%;@G1-Iag*7S+F230$Y;=0{m%9|B1>{ae z7p3>w`w*)3(myjjXhk`fGw0CI7K!XM48EmU!LEoPyMzV)=^fpn#&mNMkY7TR zz_h78KbCT1)REsc_#Mi50|V}ozGLT`MkG0|0?#|r-`-3t7`!fp@)ueD4{rtuz=rsj zJNqYq{+p!#=lJiqx~c*s01@K9ht>b8^6TGe^xv2N*}^}qi*A#YcT$&h5y5csw%+1{@oSyUyJ9jo`7V3pZ*U^rGo "sources.zip", binaries -> "binaries.jar"), submission) + submission + }, + artifactClassifier in packageSourcesOnly := Some("sources"), + artifact in (Compile, packageBinWithoutResources) ~= (art => art.withName(art.name + "-without-resources")) + ) ++ + inConfig(Compile)( + Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++ + Defaults.packageTaskSettings(packageBinWithoutResources, Def.task { + val relativePaths = + (unmanagedResources in Compile).value.flatMap(Path.relativeTo((unmanagedResourceDirectories in Compile).value)(_)) + (mappings in (Compile, packageBin)).value.filterNot { case (_, path) => relativePaths.contains(path) } + }) + ) + + val maxSubmitFileSize = { + val mb = 1024 * 1024 + 10 * mb + } + + /** Check that the jar exists, isn't empty, isn't crazy big, and can be read + * If so, encode jar as base64 so we can send it to Coursera + */ + def prepareJar(jar: File, s: TaskStreams): String = { + val errPrefix = "Error submitting assignment jar: " + val fileLength = jar.length() + if (!jar.exists()) { + s.log.error(errPrefix + "jar archive does not exist\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength == 0L) { + s.log.error(errPrefix + "jar archive is empty\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength > maxSubmitFileSize) { + s.log.error(errPrefix + "jar archive is too big. Allowed size: " + + maxSubmitFileSize + " bytes, found " + fileLength + " bytes.\n" + + jar.getAbsolutePath) + failSubmit() + } else { + val bytes = new Array[Byte](fileLength.toInt) + val sizeRead = try { + val is = new FileInputStream(jar) + val read = is.read(bytes) + is.close() + read + } catch { + case ex: IOException => + s.log.error(errPrefix + "failed to read sources jar archive\n" + ex.toString) + failSubmit() + } + if (sizeRead != bytes.length) { + s.log.error(errPrefix + "failed to read the sources jar archive, size read: " + sizeRead) + failSubmit() + } else encodeBase64(bytes) + } + } + + /** Task to package solution to a given file path */ + lazy val packageSubmissionSetting = packageSubmission := { + val args: Seq[String] = Def.spaceDelimited("[path]").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val base64Jar = prepareJar(jar, s) + + val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath) + scala.tools.nsc.io.File(path).writeAll(base64Jar) + } + +/* + /** Task to submit a solution to coursera */ + val submit = inputKey[Unit]("submit solution to Coursera") + lazy val submitSetting = submit := { + val args: Seq[String] = Def.spaceDelimited("").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val assignmentDetails = assignmentInfo.value + val assignmentKey = assignmentDetails.key + val courseName = + course.value match { + case "capstone" => "scala-capstone" + case "bigdata" => "scala-spark-big-data" + case other => other + } + + val partId = assignmentDetails.partId + val itemId = assignmentDetails.itemId + val premiumItemId = assignmentDetails.premiumItemId + + val (email, secret) = args match { + case email :: secret :: Nil => + (email, secret) + case _ => + val inputErr = + s"""|Invalid input to `submit`. The required syntax for `submit` is: + |submit + | + |The submit token is NOT YOUR LOGIN PASSWORD. + |It can be obtained from the assignment page: + |https://www.coursera.org/learn/$courseName/programming/$itemId + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + """.stripMargin + s.log.error(inputErr) + failSubmit() + } + + val base64Jar = prepareJar(jar, s) + val json = + s"""|{ + | "assignmentKey":"$assignmentKey", + | "submitterEmail":"$email", + | "secret":"$secret", + | "parts":{ + | "$partId":{ + | "output":"$base64Jar" + | } + | } + |}""".stripMargin + + def postSubmission[T](data: String): Try[HttpResponse[String]] = { + val http = Http("https://www.coursera.org/api/onDemandProgrammingScriptSubmissions.v1") + val hs = List( + ("Cache-Control", "no-cache"), + ("Content-Type", "application/json") + ) + s.log.info("Connecting to Coursera...") + val response = Try(http.postData(data) + .headers(hs) + .option(HttpOptions.connTimeout(10000)) // scalaj default timeout is only 100ms, changing that to 10s + .asString) // kick off HTTP POST + response + } + + val connectMsg = + s"""|Attempting to submit "${assignment.value}" assignment in "$courseName" course + |Using: + |- email: $email + |- submit token: $secret""".stripMargin + s.log.info(connectMsg) + + def reportCourseraResponse(response: HttpResponse[String]): Unit = { + val code = response.code + val respBody = response.body + + /* Sample JSON response from Coursera + { + "message": "Invalid email or token.", + "details": { + "learnerMessage": "Invalid email or token." + } + } + */ + + // Success, Coursera responds with 2xx HTTP status code + if (response.is2xx) { + val successfulSubmitMsg = + s"""|Successfully connected to Coursera. (Status $code) + | + |Assignment submitted successfully! + | + |You can see how you scored by going to: + |https://www.coursera.org/learn/$courseName/programming/$itemId/ + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + |and clicking on "My Submission".""".stripMargin + s.log.info(successfulSubmitMsg) + } + + // Failure, Coursera responds with 4xx HTTP status code (client-side failure) + else if (response.is4xx) { + val result = Try(Json.parse(respBody)).toOption + val learnerMsg = result match { + case Some(resp: JsObject) => + (JsPath \ "details" \ "learnerMessage").read[String].reads(resp).get + case Some(x) => // shouldn't happen + "Could not parse Coursera's response:\n" + x + case None => + "Could not parse Coursera's response:\n" + respBody + } + val failedSubmitMsg = + s"""|Submission failed. + |There was something wrong while attempting to submit. + |Coursera says: + |$learnerMsg (Status $code)""".stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera responds with 5xx HTTP status code (server-side failure) + else if (response.is5xx) { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera seems to be unavailable at the moment (Status $code) + |Check https://status.coursera.org/ and try again in a few minutes. + """.stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera repsonds with an unexpected status code + else { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera replied with an unexpected code (Status $code) + """.stripMargin + s.log.error(failedSubmitMsg) + } + } + + // kick it all off, actually make request + postSubmission(json) match { + case Success(resp) => reportCourseraResponse(resp) + case Failure(e) => + val failedConnectMsg = + s"""|Connection to Coursera failed. + |There was something wrong while attempting to connect to Coursera. + |Check your internet connection. + |${e.toString}""".stripMargin + s.log.error(failedConnectMsg) + } + + } +*/ + + def failSubmit(): Nothing = { + sys.error("Submission failed") + } + + /** + * ***************** + * DEALING WITH JARS + */ + def encodeBase64(bytes: Array[Byte]): String = + new String(Base64.encodeBase64(bytes)) +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..c0bab04 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.8 diff --git a/project/buildSettings.sbt b/project/buildSettings.sbt new file mode 100644 index 0000000..f7847f8 --- /dev/null +++ b/project/buildSettings.sbt @@ -0,0 +1,7 @@ +// Used for base64 encoding +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" + +// Used for Coursera submussion +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.3.0" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.9" + diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..64a2492 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("io.get-coursier" % "sbt-coursier" % "2.0.0-RC3-5") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.3.4") diff --git a/src/main/scala/newInterpreter/Logger.scala b/src/main/scala/newInterpreter/Logger.scala new file mode 100644 index 0000000..617c782 --- /dev/null +++ b/src/main/scala/newInterpreter/Logger.scala @@ -0,0 +1,16 @@ +package newInterpreter + +object Logger + private var logging = false + private var indentation = 0 + def on(): Unit = + logging = true + indentation = 0 + def off(): Unit = + logging = false + def indent(): Unit = + indentation = indentation + 1 + def unindent(): Unit = + indentation = indentation - 1 + def log(s: => String): Unit = + if logging then println("| " * indentation + s) diff --git a/src/main/scala/newInterpreter/RecursiveLanguage.scala b/src/main/scala/newInterpreter/RecursiveLanguage.scala new file mode 100644 index 0000000..5a9cc99 --- /dev/null +++ b/src/main/scala/newInterpreter/RecursiveLanguage.scala @@ -0,0 +1,226 @@ +package newInterpreter + +object RecursiveLanguage { + /** Expression tree, also called Abstract Syntax Tree (AST) */ + enum Expr + case Constant(value: Int) + case Name(name: String) + case BinOp(op: BinOps, arg1: Expr, arg2: Expr) + case IfNonzero(cond: Expr, caseTrue: Expr, caseFalse: Expr) + case Call(function: Expr, arg: Expr) + case Fun(param: String, body: Expr) + + /** The empty list, also known as nil. */ + case Empty + + /** A compound data type composed of a head and a tail. */ + case Cons(head: Expr, tail: Expr) + + /** A pattern matching expression for Empty and Cons. */ + case Match(scrutinee: Expr, caseEmpty: Expr, headName: String, tailName: String, caseCons: Expr) + import Expr._ + + /** Primitive operations that operation on constant values. */ + enum BinOps + case Plus, Minus, Times, DividedBy, Modulo, LessEq + import BinOps._ + + def evalBinOp(op: BinOps)(ex: Expr, ey: Expr): Expr = + (op, ex, ey) match + case (Plus, Constant(x), Constant(y)) => Constant(x + y) + case (Minus, Constant(x), Constant(y)) => Constant(x - y) + case (Times, Constant(x), Constant(y)) => Constant(x * y) + case (LessEq, Constant(x), Constant(y)) => Constant(if x <= y then 1 else 0) + case (Modulo, Constant(x), Constant(y)) => if y == 0 then error("Division by zero") else Constant(x % y) + case (DividedBy, Constant(x), Constant(y)) => if y == 0 then error("Division by zero") else Constant(x / y) + case _ => error(s"Type error in ${show(BinOp(op, ex, ey))}") + + type DefEnv = Map[String, Expr] + + /** Evaluates a progam e given a set of top level definition defs */ + def eval(e: Expr, defs: DefEnv): Expr = + e match + case Constant(c) => e + case Name(n) => + defs.get(n) match + case None => error(s"Unknown name $n") + case Some(body) => eval(body, defs) + case BinOp(op, e1, e2) => + evalBinOp(op)(eval(e1, defs), eval(e2, defs)) + case IfNonzero(cond, caseTrue, caseFalse) => + if eval(cond, defs) != Constant(0) then eval(caseTrue, defs) + else eval(caseFalse, defs) + case Fun(n, body) => e + case Call(fun, arg) => + Logger.log(show(e)) + Logger.indent() + val eFun = eval(fun, defs) + val eArg = eval(arg, defs) + eFun match + case Fun(n, body) => + Logger.unindent() + Logger.log(s"FUN: ${show(eFun)} ARG: ${show(eArg)}") + val bodySub = subst(body, n, eArg) + Logger.log(s"${show(bodySub)}") + Logger.indent() + val res = eval(bodySub, defs) + Logger.unindent() + Logger.log(s"+--> ${show(res)}") + res + case _ => error(s"Cannot apply non-function ${show(eFun)} in a call") + + + /** Substitutes Name(n) by r in e. */ + def subst(e: Expr, n: String, r: Expr): Expr = + e match + case Constant(c) => e + case Name(s) => if s == n then r else e + case BinOp(op, e1, e2) => + BinOp(op, subst(e1, n, r), subst(e2, n, r)) + case IfNonzero(cond, trueE, falseE) => + IfNonzero(subst(cond, n, r), subst(trueE, n, r), subst(falseE, n, r)) + case Call(f, arg) => + Call(subst(f, n, r), subst(arg, n, r)) + case Fun(param, body) => + // If n conflicts with param, there cannot be a reference to n in the + // function body, there is nothing so substite. + if param == n then e + else + val fvs = freeVars(r) + // If the free variables in r contain param the naive substitution would + // change the meaning of param to reference to the function parameter. + if fvs.contains(param) then + // Perform alpha conversion in body to eliminate the name collision. + val param1 = differentName(param, fvs) + val body1 = alphaConvert(body, param, param1) + Fun(param1, subst(body1, n, r)) + else + // Otherwise, substitute in the function body anyway. + Fun(param, subst(body, n, r)) + case Empty => ??? + case Cons(head, tail) => ??? + case Match(scrutinee, caseEmpty, headName, tailName, caseCons) => + if headName == n || tailName == n then + // If n conflicts with headName or tailName, there cannot be a reference + // to n in caseCons. Simply substite n by r in scrutinee and caseEmpty. + ??? + else + // If the free variables in r contain headName or tailName, the naive + // substitution would change their meaning to reference to pattern binds. + + // Perform alpha conversion in caseCons to eliminate the name collision. + + // Otherwise, substitute in scrutinee, caseEmpty & caseCons anyway. + ??? + + def differentName(n: String, s: Set[String]): String = + if s.contains(n) then differentName(n + "'", s) + else n + + /** Computes the set of free variable in e. */ + def freeVars(e: Expr): Set[String] = + e match + case Constant(c) => Set() + case Name(s) => Set(s) + case BinOp(op, e1, e2) => freeVars(e1) ++ freeVars(e2) + case IfNonzero(cond, trueE, falseE) => freeVars(cond) ++ freeVars(trueE) ++ freeVars(falseE) + case Call(f, arg) => freeVars(f) ++ freeVars(arg) + case Fun(param, body) => freeVars(body) - param + // TODO: Add cases for Empty, Cons & Match + + /** Substitutes Name(n) by Name(m) in e. */ + def alphaConvert(e: Expr, n: String, m: String): Expr = + e match + case Constant(c) => e + case Name(s) => if s == n then Name(m) else e + case BinOp(op, e1, e2) => + BinOp(op, alphaConvert(e1, n, m), alphaConvert(e2, n, m)) + case IfNonzero(cond, trueE, falseE) => + IfNonzero(alphaConvert(cond, n, m), alphaConvert(trueE, n, m), alphaConvert(falseE, n, m)) + case Call(f, arg) => + Call(alphaConvert(f, n, m), alphaConvert(arg, n, m)) + case Fun(param, body) => + // If n conflicts with param, there cannot be references to n in body, + // as these would reference param instead. + if param == n then e + else Fun(param, alphaConvert(body, n, m)) + // TODO: Add cases for Empty, Cons & Match + + case class EvalException(msg: String) extends Exception(msg) + + def error(msg: String) = throw EvalException(msg) + + // Printing and displaying + + /** Pretty print an expression as a String. */ + def show(e: Expr): String = + e match + case Constant(c) => c.toString + case Name(n) => n + case BinOp(op, e1, e2) => + val opString = op match + case Plus => "+" + case Minus => "-" + case Times => "*" + case DividedBy => "/" + case Modulo => "%" + case LessEq => "<=" + s"($opString ${show(e1)} ${show(e2)})" + case IfNonzero(cond, caseTrue, caseFalse) => + s"(if ${show(cond)} then ${show(caseTrue)} else ${show(caseFalse)})" + case Call(f, arg) => show(f) + "(" + show(arg) + ")" + case Fun(n, body) => s"($n => ${show(body)})" + + /** Pretty print top-level definition as a String. */ + def showEnv(env: Map[String, Expr]): String = + env.map { case (name, body) => s"def $name =\n ${show(body)}" }.mkString("\n\n") + "\n" + + /** Evaluates an expression with the given top-level definitions with logging enabled. */ + def tracingEval(e: Expr, defs: DefEnv): Expr = + Logger.on() + val evaluated = eval(e, defs) + println(s" ~~> $evaluated\n") + Logger.off() + evaluated + + def minus(e1: Expr, e2: Expr) = BinOp(BinOps.Minus, e1, e2) + def plus(e1: Expr, e2: Expr) = BinOp(BinOps.Plus, e1, e2) + def leq(e1: Expr, e2: Expr) = BinOp(BinOps.LessEq, e1, e2) + def times(e1: Expr, e2: Expr) = BinOp(BinOps.Times, e1, e2) + def modulo(e1: Expr, e2: Expr) = BinOp(BinOps.Modulo, e1, e2) + def dividedBy(e1: Expr, e2: Expr) = BinOp(BinOps.DividedBy, e1, e2) + // The {Name => N} import syntax renames Name to N in this scope + import Expr.{Name => N, Constant => C, _} + + /** Examples of top-level definitions (used in tests) */ + val definitions: DefEnv = Map[String, Expr]( + "fact" -> Fun("n", + IfNonzero(N("n"), + times(N("n"), + Call(N("fact"), minus(N("n"), C(1)))), + C(1))), + + "square" -> Fun("x", + times(N("x"), N("x"))), + + "twice" -> Fun("f", Fun("x", + Call(N("f"), Call(N("f"), N("x"))))), + + // TODO Implement map (see recitation session) + "map" -> Empty, + + // TODO Implement gcd (see recitation session) + "gcd" -> Empty, + + // TODO Implement foldLeft (see recitation session) + "foldLeft" -> Empty, + + // TODO Implement foldRight (analogous to foldLeft, but operate right-to-left) + "foldRight" -> Empty, + ) + + def main(args: Array[String]): Unit = + println(showEnv(definitions)) + tracingEval(Call(N("fact"), C(6)), definitions) + tracingEval(Call(Call(N("twice"), N("square")), C(3)), definitions) +} diff --git a/src/test/scala/newInterpreter/RecursiveLanguageSuite.scala b/src/test/scala/newInterpreter/RecursiveLanguageSuite.scala new file mode 100644 index 0000000..cd22555 --- /dev/null +++ b/src/test/scala/newInterpreter/RecursiveLanguageSuite.scala @@ -0,0 +1,126 @@ +package newInterpreter + +class RecursiveLanguageSuite { + import org.junit._ + import org.junit.Assert.{assertEquals, fail} + import RecursiveLanguage._, Expr.{Constant => C, Name => N, _} + + @Test def gcdTests(): Unit = { + def test(a: Int, b: Int, c: Int): Unit = { + val call = Call(Call(N("gcd"), C(a)), C(b)) + val result = eval(call, definitions) + assertEquals(C(c), result) + } + test(60, 90, 30) + test(36, 48, 12) + test(25, 85, 5 ) + test(12, 15, 3 ) + test(60, 75, 15) + test(35, 56, 7 ) + test(96, 128, 32) + test(120, 135, 15) + test(150, 225, 75) + } + + @Test def evalTests: Unit = { + assertEquals(Empty, eval(Empty, Map())) + assertEquals(Cons(C(1), Empty), eval(Cons(C(1), Empty), Map())) + assertEquals(Cons(C(2), Empty), eval(Cons(BinOp(BinOps.Plus, C(1), C(1)), Empty), Map())) + + assertEquals(C(0), eval(Match(Empty, C(0), "_", "_", C(1)), Map())) + assertEquals(C(1), eval(Match(Cons(Empty, Empty), C(0), "_", "_", C(1)), Map())) + assertEquals(C(2), eval(Match(Cons(C(2), Empty), C(0), "x", "xs", N("x")), Map())) + assertEquals(Empty, eval(Match(Cons(C(2), Empty), C(0), "x", "xs", N("xs")), Map())) + + try { + eval(Match(C(0), C(0), "_", "_", C(1)), Map()) + fail() + } catch { + case EvalException(msg) => // OK! + } + } + + @Test def freeVarsTests: Unit = { + assertEquals(Set(), freeVars(Empty)) + assertEquals(Set("a", "b"), freeVars(Cons(N("a"), N("b")))) + assertEquals(Set(), freeVars(Match(Empty, C(0), "a", "b", N("a")))) + assertEquals(Set(), freeVars(Match(Empty, C(0), "a", "b", N("b")))) + assertEquals(Set("c"), freeVars(Match(Empty, N("c"), "_", "_", C(1)))) + } + + @Test def alphaConvertTests: Unit = { + assertEquals(Cons(N("b"), N("b")), alphaConvert(Cons(N("a"), N("a")), "a", "b")) + assertEquals(Match(Empty, C(0), "a", "b", N("a")), alphaConvert(Match(Empty, C(0), "a", "b", N("a")), "a", "b")) + assertEquals(Match(Empty, C(0), "a", "b", N("b")), alphaConvert(Match(Empty, C(0), "a", "b", N("b")), "a", "b")) + assertEquals(Match(Empty, N("a"), "a", "b", C(1)), alphaConvert(Match(Empty, N("c"), "a", "b", C(1)), "c", "a")) + assertEquals(Match(Empty, N("b"), "a", "b", C(1)), alphaConvert(Match(Empty, N("c"), "a", "b", C(1)), "c", "b")) + assertEquals(Match(Empty, C(0), "a", "b", N("e")), alphaConvert(Match(Empty, C(0), "a", "b", N("d")), "d", "e")) + } + + val sum = "sum" -> Fun("a", Fun("b", BinOp(BinOps.Plus, N("a"), N("b")))) + val div = "div" -> Fun("a", Fun("b", BinOp(BinOps.DividedBy, N("a"), N("b")))) + + @Test def foldLeft: Unit = { + val list1 = Cons(C(1), Cons(C(2), Cons(C(3), Empty))) + val call1 = Call(Call(Call(N("foldLeft"), list1), C(100)), N("sum")) + assertEquals(C(106), eval(call1, definitions + sum)) + + val list2 = Cons(C(1), Cons(C(2), Cons(C(3), Cons(C(4), Cons(C(5), Cons(C(6), Empty)))))) + val call2 = Call(Call(Call(N("foldLeft"), list2), C(100000)), N("div")) + assertEquals(C(138), eval(call2, definitions + div)) + } + + @Test def foldRight: Unit = { + val list1 = Cons(C(1), Cons(C(2), Cons(C(3), Empty))) + val call1 = Call(Call(Call(N("foldRight"), list1), C(100)), N("sum")) + assertEquals(C(106), eval(call1, definitions + sum)) + + val list2 = Cons(C(1000000), Cons(C(20000), Cons(C(3000), Cons(C(400), Cons(C(50), Cons(C(6), Empty)))))) + val call2 = Call(Call(Call(N("foldRight"), list2), C(1)), N("div")) + assertEquals(C(3003), eval(call2, definitions + div)) + } + + @Test def substitutionSimple: Unit = { + // Substitution should happend everywhere in the Match. In scrutinee and in caseCons: + assertEquals(Cons(C(1), Empty), eval( + Call(N("bar"), Cons(C(1), Empty)), + Map("bar" -> Fun("x", Match(N("x"), C(0), "y", "z", N("x")))) + )) + + // In scrutinee and in caseEmpty: + assertEquals(Empty, eval( + Call(N("bar"), Empty), + Map("bar" -> Fun("x", Match(N("x"), N("x"), "y", "z", C(0)))) + )) + + // But not inside caseCons when the binding name clashes with the functions name: + assertEquals(C(1), eval( + Call(N("bar"), Cons(C(1), Empty)), + Map("bar" -> Fun("x", Match(N("x"), C(0), "x", "z", N("x")))) + )) + } + + @Test def substitutionFunctionCapture: Unit = { + // Here comes the real fun, plus_one uses "map" as it's first argument name, + // incorrect implementation will accidentally capture the recursion in map + // turn into that name into a reference to first argument of plus_one. + val plus_one = "plus_one" -> Fun("map", BinOp(BinOps.Plus, C(1), N("map"))) + val list1 = Cons(C(1), Cons(C(2), Cons(C(3), Empty))) + val list2 = Cons(C(2), Cons(C(3), Cons(C(4), Empty))) + assertEquals(list2, eval(Call(Call(N("map"), list1), N("plus_one")), definitions + plus_one)) + } + + @Test def substitutionMatchCapture: Unit = { + // More fun, this uses "fact" as a first binding in the pattern match: + val pairMap = "pairMap" -> Fun("pair", Fun("function", + Match( + N("pair"), + Empty, + "fact", "tcaf", + Cons(Call(N("function"), N("fact")), Call(N("function"), N("tcaf")))) + )) + assertEquals(Cons(C(6), C(24)), eval(Call(Call(N("pairMap"), Cons(C(3), C(4))), N("fact")), definitions + pairMap)) + } + + @Rule def individualTestTimeout = new org.junit.rules.Timeout(200 * 1000) +} diff --git a/student.sbt b/student.sbt new file mode 100644 index 0000000..855fa0c --- /dev/null +++ b/student.sbt @@ -0,0 +1,9 @@ +// Used for base64 encoding +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" + +// Used for Coursera submussion +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4" + +// Student tasks (i.e. packageSubmission) +enablePlugins(StudentTasks)