diff --git a/META-INF/RASCAL.MF b/META-INF/RASCAL.MF index d3977080a63..a92afc8e206 100644 --- a/META-INF/RASCAL.MF +++ b/META-INF/RASCAL.MF @@ -1,5 +1,5 @@ Project-Name: rascal -Source: src/org/rascalmpl/library,test/org/rascalmpl/benchmark,test//org/rascalmpl/test/data +Source: src/org/rascalmpl/library,src/org/rascalmpl/tutor,test/org/rascalmpl/benchmark,test/org/rascalmpl/test/data Courses: src/org/rascalmpl/courses diff --git a/pom.xml b/pom.xml index 2781f02313a..2ed31386d1d 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ scm:git:ssh://git@github.com/usethesource/rascal.git - v0.41.0-RC1 + HEAD @@ -137,6 +137,7 @@ ${project.build.outputDirectory} ${project.basedir}/src/org/rascalmpl/library + ${project.basedir}/src/org/rascalmpl/tutor |std:///| ${project.basedir}/FUNDING diff --git a/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java index 875327dde1c..c05f62add04 100644 --- a/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java +++ b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java @@ -180,6 +180,11 @@ public void cancelRunningCommandRequested() { eval.endAllJobs(); } + public void cleanEnvironment() { + Objects.requireNonNull(eval, "Not initialized yet"); + eval.getCurrentModuleEnvironment().reset(); + } + @Override public ICommandOutput stackTraceRequested() { Objects.requireNonNull(eval, "Not initialized yet"); diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/Compiler.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/Compiler.rsc new file mode 100644 index 00000000000..3131a42534f --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/Compiler.rsc @@ -0,0 +1,977 @@ +@bootstrapParser +@synopsis{compiles .rsc and .md files to markdown by executing Rascal-specific code and inlining its output} +@description{ + This compiler collects .rsc files and .md files from a PathConfig's srcs folders. + + Every .rsc file is compiled to a .md file with an outline of the declarations contained + in the file and the contents of the @synopsis, @description, @pitfalls, @benefits, @examples + tags with those declarations. @doc is also supported for backward compatibility's purposes. + The resulting markdown is processed by the rest of the compiler, as if written by hand. + + Every .md file is scanned for rascal-shell between triple backticks elements. The contents between the backticks are + executed by a private Rascal REPL and the output is captured in different ways. Normal IO + via stderr and stdout is literally printed back and HTML or image output is inlined into + the document. + + For (nested) folders in the srcs folders, which do not contain an `index.md` file, or + a `.md` file where the name is equal to the name of the current folder, a fresh index.md + file is generated. +} +module lang::rascal::tutor::Compiler + +import Message; +import Exception; +import IO; +import String; +import Node; +import List; +import Relation; +import Location; +import ParseTree; +import util::Reflective; +import util::FileSystem; +import ValueIO; + +import lang::yaml::Model; +import lang::rascal::tutor::repl::TutorCommandExecutor; +import lang::rascal::tutor::apidoc::GenerateMarkdown; +import lang::rascal::tutor::apidoc::ExtractInfo; +import lang::rascal::tutor::Indexer; +import lang::rascal::tutor::Names; +import lang::rascal::tutor::Output; +import lang::rascal::tutor::Includer; +import lang::rascal::\syntax::Rascal; + +public PathConfig defaultConfig + = pathConfig( + bin=|target://rascal-tutor/docs|, + libs=[|lib://rascal|], + srcs=[ + |project://rascal-tutor/src/lang/rascal/tutor/examples/Test| + ]); + +public list[Message] lastErrors = []; + +public void defaultCompile(bool clean=false) { + if (clean) { + remove(defaultConfig.bin, recursive=true); + } + errors = compile(defaultConfig); + + for (e <- errors) { + println(": + ' <}>"); + } + + lastErrors = errors; +} + +@synopsis{compiles each pcfg.srcs folder as a course root} +list[Message] compile(PathConfig pcfg, CommandExecutor exec = createExecutor(pcfg)) { + ind = createConceptIndex(pcfg); + + if (pcfg.isPackageCourse) { + generatePackageIndex(pcfg); + } + else { + storeImportantProjectMetaData(pcfg); + } + + // remove trailing slashes + pcfg.ignores = [i.parent + i.file | i <- pcfg.ignores]; + + return [*compileCourse(src, pcfg[currentRoot=src], exec, ind) | src <- pcfg.srcs]; +} + +void storeImportantProjectMetaData(PathConfig pcfg) { + // these files are with the .txt extension such that they are not automatically + // incorporated into the website. Rather other pages can include them where they see fit. + // this information, however, is not easy to obtain outside of the build + // environment of the current project. Therefore we store it here and now. + + if (!pcfg.packageName?) { + return; + } + + if (pcfg.license? && exists(pcfg.license)) { + copy(pcfg.license, pcfg.bin + "LICENSE_.txt"); + } + + if (pcfg.citation? && exists(pcfg.citation)) { + copy(pcfg.citation, pcfg.bin + "CITATION_.md"); + } + + if (pcfg.funding? && exists(pcfg.funding)) { + copy(pcfg.funding, pcfg.bin + "FUNDING_.md"); + } + + if (pcfg.releaseNotes? && exists(pcfg.releaseNotes)) { + copy(pcfg.releaseNotes, pcfg.bin + "RELEASE-NOTES_.md"); + } + + dependencies = [ f | f <- pcfg.classloaders, exists(f), f.extension=="jar"]; + + if (dependencies != []) { + writeFile(pcfg.bin + "DEPENDENCIES_.txt", + " * + '<}> + " + ); + } +} + +void generatePackageIndex(PathConfig pcfg) { + targetFile = pcfg.bin + "Packages" + package(pcfg.packageName) + "index.md"; + + if (pcfg.license?) { + writeFile(targetFile.parent + "License.md", + "--- + 'title: License + '--- + ' + '"); + } + + if (pcfg.funding?) { + writeFile(targetFile.parent + "Funding.md", + "--- + 'title: Funding + '--- + ' + ':::info + 'Open-source software is free for use, yet it does not come for free. + 'The following sources of funding have been instrumental in the creation + 'and maintenance of . You may consider also to become + 'a [sponsor](https://github.com/sponsors/usethesource?o=esb) + '::: + ' + '"); + } + + if (pcfg.citation?) { + writeFile(targetFile.parent + "Citation.md", + "--- + 'title: Citation + '--- + ' + ':::info + 'Open-source software is [citeable](https://www.software.ac.uk/how-cite-software) output of research and development efforts. + 'Citing software **recognizes** the associated investment and the quality of the result. + 'If you use open-source software, it is becoming standard practise to recognize the work as + 'its authors have indicated below. In turn their effort might be **awarded** with renewed [funding](../../Packages//Funding.md)<} else {>funding<}> for + 'based on the evidence of your appreciation, and it may help their individual career perspectives. + '::: + ' + '"); + } + + if (pcfg.releaseNotes?) { + writeFile(targetFile.parent + "RELEASE-NOTES.md", + "--- + 'title: Release notes + '--- + ' + '"); + } + + dependencies = [ f | f <- pcfg.classloaders, exists(f), f.extension=="jar"]; + + if (dependencies != []) { + writeFile(targetFile.parent + "Dependencies.md", + "--- + 'title: Dependencies + '--- + ' + 'These are compile-time and run-time dependencies of : + ' + ' * + '<}> + ' + ':::info + 'You should check that the licenses of the above dependencies are compatible with your goals and situation. The authors and owners of cannot be held liable for any damages caused by the use of those licenses, or changes therein. + ' + 'The authors contributing to do prefer open-source licenses for their dependencies that are permissive to commercial exploitation and any kind of reuse, and that are non-viral. + '::: + " + ); + } + + writeFile(targetFile.parent + "index.md", + "--- + 'title: + '--- + ' + 'This is the documentation for version of . + ' + '* [API documentation](../../Packages//API)<}> + '* [](../../Packages//) + '<}>* [Stackoverflow questions](https://stackoverflow.com/questions/tagged/rascal+) + '* [Release notes](../../Packages//RELEASE-NOTES.md)<}> + '* [Open-source license](../../Packages//License.md)<}> + '* How to [cite this software](../../Packages//Citation.md)<}> + '* [Funding sources](../../Packages//Funding.md) sources.<}> + '* [Dependencies](../../Packages//Dependencies.md)<}> + '* [Source code](<""[1..-1]>)<}> + '* [Issue tracker](<""[1..-1]>)<}> + ' + '#### Installation + ' + 'To use in a maven-based Rascal project, include the following dependency in the `pom.xml` file: + ' + '```xml + '\ + ' \ + ' \\ + ' \\ + ' \\ + ' \ + '\ + '``` + '**and** change the `Require-Libraries` field in `/path/to/yourProjectName/META-INF/RASCAL.MF` like so: + ' + '```MF + 'Manifest-Version: 0.0.1 + 'Project-Name: yourProjectName + 'Source: path/to/src + 'Require-Libraries: |lib://| + ' + ' + '``` + ':::info + 'dot.MF files _must_ end with an empty line. + '::: + "); +} + +list[Message] compileCourse(loc root, PathConfig pcfg, CommandExecutor exec, Index ind) + = compileDirectory(root, pcfg[currentRoot=root], exec, ind); + +list[Message] compile(loc src, PathConfig pcfg, CommandExecutor exec, Index ind, int sidebar_position=-1) { + if (src in pcfg.ignores) { + return [info("skipped ignored location: ", src)]; + } + + // new concept, new execution environment: + exec.reset(); + + if (isDirectory(src), src.file != "internal") { + return compileDirectory(src, pcfg, exec, ind, sidebar_position=sidebar_position); + } + else if (src.extension == "rsc") { + return compileRascalFile(src, pcfg[currentFile=src], exec, ind); + } + else if (src.extension in {"md"}) { + return compileMarkdownFile(src, pcfg, exec, ind, sidebar_position=sidebar_position); + } + else if (src.extension in {"png","jpg","svg","jpeg", "html", "js"}) { + try { + println("copying [Asset]"); + copy(src, pcfg.bin + (pcfg.isPackageCourse ? "assets/Packages/" : "assets") + capitalize(pcfg.currentRoot.file) + relativize(pcfg.currentRoot, src).path); + + return []; + } + catch IO(str message): { + return [error(message, src)]; + } + } + else { + return []; + } +} + +list[Message] compileDirectory(loc d, PathConfig pcfg, CommandExecutor exec, Index ind, int sidebar_position=-1) { + if (d in pcfg.ignores) { + return [info("skipped ignored location: ", d)]; + } + + println("compiling [Folder]"); + + indexFiles = {(d + "")[extension="md"], (d + "index.md")}; + + if (!exists(d)) { + return [warning("Course folder does not exist on disk: ", d)]; + } + + output = []; + errors = []; + nestedDtls = []; + + if (i <- indexFiles && exists(i)) { + // this can only be a markdown file (see above) + j=i; + j.file = (j.file == j.parent[extension="md"].file) ? "index.md" : j.file; + + targetFile = pcfg.bin + + (pcfg.isPackageCourse ? "Packages/" : "") + + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file)) + + relativize(pcfg.currentRoot, j)[extension="md"].path; + + if (!exists(targetFile) || lastModified(i) > lastModified(targetFile)) { + println("compiling [Index Markdown]"); + output = compileMarkdown(i, pcfg[currentFile=i], exec, ind, sidebar_position=sidebar_position); + + writeFile(targetFile, + " + '<}>" + ); + + if (details(list[str] xxx) <- output) { + // here we give the details list declared in `details` header + // on to compute the right sidebar_positions down for the nested + // concepts + nestedDtls = xxx; + } + + errors = [e | err(e) <- output]; + if (errors != []) { + writeBinaryValueFile(targetFile[extension="errors"], errors); + } + else { + remove(targetFile[extension="errors"]); + } + } + else { + println("reusing "); + if (exists(targetFile[extension="errors"])) { + errors = readBinaryValueFile(#list[Message], targetFile[extension="errors"]); + } + } + } + else { + generateIndexFile(d, pcfg, sidebar_position=sidebar_position); + } + + return [ + *errors, + *[*compile(s, pcfg, exec, ind, sidebar_position=sp) + | s <- d.ls + , !(s in pcfg.ignores) + , !(s in indexFiles) + , isDirectory(s) || s.extension in {"md","rsc","png","jpg","svg","jpeg", "html", "js"} + , int sp := indexOf(nestedDtls, capitalize(s[extension=""].file)) + ] + ]; +} + +list[Message] generateIndexFile(loc d, PathConfig pcfg, int sidebar_position=-1) { + try { + p2r = pathToRoot(pcfg.currentRoot, d, pcfg.isPackageCourse); + title = (d == pcfg.currentRoot && d.file in {"src","rascal","api"}) ? "API" : d.file; + + targetFile = pcfg.bin + + (pcfg.isPackageCourse ? "Packages/" : "") + + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file)) + + relativize(pcfg.currentRoot, d).path + + "index.md" + ; + + str slug = relativize(pcfg.bin, targetFile).parent.path; + + writeFile(targetFile, + "--- + 'title:   + 'slug: <slug> + '<if (sidebar_position != -1) {>sidebar_position: <sidebar_position> + '<}>--- + ' + '<for (loc e <- d.ls, isDirectory(e) || e.extension in {"rsc", "md"}, e.file != "internal", !(e in pcfg.ignores), !(e.file in {"index.rsc", "Index.rsc"})) {> + '* [<e[extension=""].file>](<p2r>/<if (pcfg.isPackageCourse) {>Packages/<package(pcfg.packageName)>/<}><if (pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) {>API<} else {><capitalize(pcfg.currentRoot.file)><}><relativize(pcfg.currentRoot, e)[extension=isDirectory(e)?"":"md"].path>)<}> + '<if (loc e <- d.ls, e.file in {"index.rsc", "Index.rsc"}) {>* [<e[extension=""].file>](<p2r>/<if (pcfg.isPackageCourse) {>Packages/<package(pcfg.packageName)>/<}><if (pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) {>API<} else {><capitalize(pcfg.currentRoot.file)><}><relativize(pcfg.currentRoot, e).parent.path>/module_Index.md)<}>"); + return []; + } catch IO(msg): { + return [error(msg, d)]; + } +} + +@synopsis{Translates Rascal source files to docusaurus markdown.} +list[Message] compileRascalFile(loc m, PathConfig pcfg, CommandExecutor exec, Index ind) { + loc targetFile = pcfg.bin + + (pcfg.isPackageCourse ? "Packages/<package(pcfg.packageName)>" : "") + + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file)) + + relativize(pcfg.currentRoot, m)[extension="md"].path; + + if (targetFile.file in {"index.md", "Index.md"}) { + // that would overwrite the actual index. Some modules can be named "Index.rsc or index.rsc" + // this underscore prefix is also reflected in the index builder of course! + targetFile.file = "module_Index.md"; + } + + errors = []; + + if (!exists(targetFile) || lastModified(targetFile) < lastModified(m)) { + str parentSlug = (|path:///| + (pcfg.isPackageCourse ? "Packages/<package(pcfg.packageName)>" : "") + + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file)) + + relativize(pcfg.currentRoot, m).parent.path).path; + + println("compiling <m> [Rascal Source File]"); + list[Output] output = generateAPIMarkdown(parentSlug, m, pcfg, exec, ind); + + writeFile(targetFile, + "<for (line(x) <- output) {><x> + '<}>" + ); + + errors = [e | err(e) <- output]; + if (errors != []) { + writeBinaryValueFile(targetFile[extension="errors"], errors); + } + else { + remove(targetFile[extension="errors"]); + } + } + else { + println("reusing <m>"); + if (exists(targetFile[extension="errors"])) { + errors = readBinaryValueFile(#list[Message], targetFile[extension="errors"]); + } + } + + return errors; +} + +@synopsis{This uses another nested directory listing to construct information for the TOC embedded in the current document.} +list[str] createDetailsList(loc m, PathConfig pcfg) + = sort([ "<capitalize(pcfg.currentRoot.file)>:<if (isDirectory(d), !exists(d + "index.md"), !exists((d + d.file)[extension="md"])) {>package:<}><if (d.extension == "rsc") {>module:<}><replaceAll(relativize(pcfg.currentRoot, d)[extension=""].path[1..], "/", "-")>" + | loc d <- m.parent.ls, m != d, !(d in pcfg.ignores), d.file != "index.md", isDirectory(d) || d.extension in {"rsc", "md"} + ]); + +list[Message] compileMarkdownFile(loc m, PathConfig pcfg, CommandExecutor exec, Index ind, int sidebar_position=-1) { + order = createDetailsList(m, pcfg); + + // turn A/B/B.md into A/B/index.md for better URLs in the end result (`A/B/`` is better than `A/B/B.html`) + m.file = (m.file == m.parent[extension="md"].file) ? "index.md" : m.file; + + loc targetFile = pcfg.bin + + (pcfg.isPackageCourse ? "Packages/<package(pcfg.packageName)>" : "") + + ((pcfg.isPackageCourse && pcfg.currentRoot.file in {"src","rascal","api"}) ? "API" : capitalize(pcfg.currentRoot.file)) + + relativize(pcfg.currentRoot, m)[extension="md"].path; + + errors = []; + + if (!exists(targetFile) || lastModified(m) > lastModified(targetFile)) { + println("compiling <m> [Normal Markdown]"); + list[Output] output = compileMarkdown(m, pcfg[currentFile=m], exec, ind, sidebar_position=sidebar_position) + [Output::empty()]; + + writeFile(targetFile, + "<for (line(x) <- output) {><x> + '<}>" + ); + + errors = [e | err(e) <- output]; + if (errors != []) { + writeBinaryValueFile(targetFile[extension="errors"], errors); + } + return errors; + } + else { + println("reusing <m>"); + if (exists(targetFile[extension="errors"])) { + // keep reporting the errors of the previous run, for clarity's sake + return readBinaryValueFile(#list[Message], targetFile[extension="errors"]); + } + } + + return []; +} + +list[Output] compileMarkdown(loc m, PathConfig pcfg, CommandExecutor exec, Index ind, int sidebar_position=-1) { + order = createDetailsList(m, pcfg); + + return compileMarkdown(readFileLines(m), 1, 0, pcfg[currentFile=m], exec, ind, order, sidebar_position=sidebar_position) + [Output::empty()]; +} + +@synopsis{Skip double quoted blocks} +list[Output] compileMarkdown([str first:/^\s*``````/, *block, str second:/^``````/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = [ + out(first), + *[out(b) | b <-block], + out(second), + *compileMarkdown(rest, line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + +@synopsis{Include Rascal code from Rascal source files} +list[Output] compileMarkdown([str first:/^\s*```rascal-include<rest1:.*>$/, *str components, /^\s*```/, *str rest2], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) { + return[ + Output::empty(), // must have an empty line + out("```rascal <rest1>"), + *[*prepareModuleForInclusion(item, /includeHeaders/ := rest1, /includeTests/ := rest1, pcfg) | item <- components], + Output::empty(), + out("```"), + *compileMarkdown(rest2, line + 1 + size(components) + 1, offset + length(first) + length(components), pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; +} + +@synopsis{Include Rascal REPL commands literally and execute them as side-effects in the REPL without reporting output unless there are unexpected errors.} +list[Output] compileMarkdown([str first:/^\s*```rascal-commands<rest1:.*>$/, *str block, /^\s*```/, *str rest2], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) { + str code = "<for (l <- block) {><l> + '<}>"; + + try { + commands = ([start[Commands]] code).top.commands; + + if (/continue/ !:= rest1) { + exec.reset(); + } + + stderr = ""; + + for (EvalCommand c <- commands) { + output = exec.eval("<c>"); + stderr += output["application/rascal+stderr"]?""; + } + + return [ + Output::empty(), // must have an empty line + out("```rascal <rest1>"), + *[out(l) | l <- block], + out("```"), + *[ + out(":::danger"), + *[out(errLine) | errLine <- split("\n", stderr)], + out(":::") + | /errors/ !:= rest1, filterErrors(stderr) != "" + ], + *[err(error("rascal-commands block failed: <stderr>", pcfg.currentFile(offset, 1, <line, 0>, <line, 1>))) | filterErrors(stderr) != ""], + *compileMarkdown(rest2, line + 1 + size(block) + 1, offset + length(first) + length(block), pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + } + catch ParseError(x): { + return [err(error("parse error in rascal-commands block: <x>", pcfg.currentFile(offset, 1, <line, 0>, <line, 1>)))]; + } +} + +@synopsis{execute _rascal-shell_ blocks on the REPL} +list[Output] compileMarkdown([str first:/^\s*```rascal-shell<rest1:.*>$/, *block, /^\s*```/, *str rest2], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = [ Output::empty(), // must have an empty line + out("```rascal-shell <rest1>"), + *compileRascalShell(block, /error/ := rest1, /continue/ := rest1, line+1, offset + size(first) + 1, pcfg, exec, ind), + out("```"), + *compileMarkdown(rest2, line + 1 + size(block) + 1, offset + size(first) + length(block), pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + +@synopsis{execute _rascal-shell-prepare_ blocks on the REPL} +list[Output] compileMarkdown([str first:/^\s*```rascal-prepare<rest1:.*>$/, *block, /^\s*```/, *str rest2], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = [ + *compileRascalShellPrepare(block, /continue/ := rest1, line+1, offset + size(first) + 1, pcfg, exec, ind), + *compileMarkdown(rest2, line + 1 + size(block) + 1, offset + size(first) + length(block), pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + +@synopsis{inline an itemized list of details (collected from the details YAML section in the header)} +list[Output] compileMarkdown([str first:/^\s*\(\(\(\s*TOC\s*\)\)\)\s*$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = [ + *[*compileMarkdown(["* ((<d>))"], line, offset, pcfg, exec, ind, []) | d <- dtls], + *compileMarkdown(rest, line + 1, offset + size(first), pcfg, exec, ind, [], sidebar_position=sidebar_position) + ] + + + [ + err(warning("TOC is empty. details section is missing from header?", pcfg.currentFile(offset, 1, <line, 0>, <line, 1>))) + | dtls == [] + ]; + +@synopsis{inline an itemized list of details (collected from the details YAML section in the header)} +list[Output] compileMarkdown([str first:/^\s*\(\(\(\s*TODO<msg:[^\)]*>\s*\)\)\)\s*$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = [ + out(":::caution"), + out("There is a \"TODO\" in the documentation source:"), + out("\t<msg>"), + out(first), + out(":::"), + err(warning("TODO: <trim(msg)>", pcfg.currentFile(offset, 1, <line, 0>, <line, 1>))), + *compileMarkdown(rest, line + 1, offset + size(first), pcfg, exec, ind, [], sidebar_position=sidebar_position) + ]; + +@synopsis{Inline example files literally, in Rascal loc notation, but do not compile further from there. Works only if positioned on a line by itself.} +list[Output] compileMarkdown([str first:/^\s*\(\(\|<url:[^\|]+>\|\)\)\s*$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) { + try { + return [ + *[out(l) | str l <- split("\n", readFile(readTextValueString(#loc, "|<url>|")))], + *compileMarkdown(rest, line + 1, offset + size(first), pcfg, exec, ind, [], sidebar_position=sidebar_position) + ]; + } + catch value x: { + return [ + err(error("Could not read <url> for inclusion: <x>", pcfg.currentFile(offset, 1, <line, 1>, <line, 2>))), + *compileMarkdown(rest, line + 1, offset + size(first), pcfg, exec, ind, [], sidebar_position=sidebar_position) + ]; + } +} + +@synopsis{implement subscript syntax for [aeh-pr-vx] (the subscript alphabet is incomplete in unicode)} +list[Output] compileMarkdown([/^<prefix:.*>~<digits:[aeh-pr-vx0-9\(\)+\-]+>~<postfix:.*>$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = compileMarkdown(["<prefix><for (ch <- chars(digits)) {><subscripts["<char(ch)>"]><}><postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + +@synopsis{detect unsupported subscripts} +list[Output] compileMarkdown([/^<prefix:.*>~<digits:[^~]*[^aeh-pr-vx0-9]+[^~]*>~<postfix:.*>$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = [ + err(error("Unsupported subscript character in <digits>", pcfg.currentFile(offset, 1, <line, 1>, <line, 2>))), + *compileMarkdown(["<prefix><digits><postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + +@synopsis{Resolve labeled links} +list[Output] compileMarkdown([/^<prefix:.*>\[<title:[^\]]*>\]\(\(<link:[A-Za-z0-9\-\ \t\.\:]+>\)\)<postfix:.*>$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) { + resolution = ind[removeSpaces(link)]; + p2r = pathToRoot(pcfg.currentRoot, pcfg.currentFile, pcfg.isPackageCourse); + + if (trim(title) == "") { + title = link; + } + + switch (resolution) { + case {str u}: { + u = /^\/assets/ := u ? u : "<p2r><u>"; + return compileMarkdown(["<prefix>[<title>](<u>)<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + case { }: { + if (/^<firstWord:[A-Za-z0-9\-\.\:]+>\s+<secondWord:[A-Za-z0-9\-\.\:]+>/ := link) { + // give this a second chance, in reverse + return compileMarkdown(["<prefix>[<title>]((<secondWord>-<firstWord>))<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + + return [ + err(error("Broken concept link: <link>", pcfg.currentFile(offset, 1, <line,0>,<line,1>))), + *compileMarkdown(["<prefix>_(<title>) <link> (broken link)_<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + } + case {_, _, *_}: { + // ambiguous resolution, first try and resolve within the current course: + if ({str u} := ind["<capitalize(pcfg.currentRoot.file)>:<removeSpaces(link)>"]) { + u = /^\/assets/ := u ? u : "<p2r><u>"; + return compileMarkdown(["<prefix>[<title>](<u>)<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + else if ({str u} := ind["<capitalize(pcfg.currentRoot.file)>-<removeSpaces(link)>"]) { + u = /^\/assets/ := u ? u : "<p2r><u>"; + return compileMarkdown(["<prefix>[<title>](<u>)<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + // or we check if its one of the details of the current concept + else if ({str u} := ind["<capitalize(pcfg.currentRoot.file)>:<fragment(pcfg.currentRoot, pcfg.currentFile)>-<removeSpaces(link)>"]) { + u = /^\/assets/ := u ? u : "<p2r><u>"; + return compileMarkdown(["<prefix>[<title>](<u>)<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + + return [ + err(error("Ambiguous concept link: <removeSpaces(link)> resolves to all of these: <for (r <- resolution) {><r> <}>", pcfg.currentFile(offset, 1, <line,0>,<line,1>), + cause="Please choose from the following options to disambiguate: <for (<str k, str v> <- rangeR(ind, ind[removeSpaces(link)]), {_} := ind[k]) {> + ' <k> resolves to <v><}>")), + *compileMarkdown(["<prefix> **broken:<link> (ambiguous)** <postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + } + } + + return [err(error("Unexpected state of link resolution for <link>: <resolution>", pcfg.currentFile(offset, 1, <line,0>,<line,1>)))]; +} + +@synopsis{Resolve unlabeled links} +default list[Output] compileMarkdown([/^<prefix:.*>\(\(<link:[A-Za-z0-9\-\ \t\.\:]+>\)\)<postfix:.*>$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) { + resolution = ind[removeSpaces(link)]; + p2r = pathToRoot(pcfg.currentRoot, pcfg.currentFile, pcfg.isPackageCourse); + + switch (resolution) { + case {u}: { + u = /^\/assets/ := u ? u : "<p2r><u>"; + return compileMarkdown(["<prefix>[<addSpaces(link)>](<u>)<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + case { }: { + if (/^<firstWord:[A-Za-z0-9\-\.\:]+>\s+<secondWord:[A-Za-z0-9\-\.\:]+>/ := link) { + // give this a second chance, in reverse + return compileMarkdown(["<prefix>((<secondWord>-<firstWord>))<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + + return [ + err(error("Broken concept link: <link>", pcfg.currentFile(offset, 1, <line,0>,<line,1>))), + *compileMarkdown(["<prefix>_<link> (broken link)_<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + } + case {str plink, /<qlink:.*>\/index\.md/}: + if (plink == qlink) { + return compileMarkdown(["<prefix>[<addSpaces(link)>](<p2r><plink>/)<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + else { + fail; + } + + case {_, _, *_}: { + // ambiguous resolution, first try and resolve within the current course: + if ({u} := ind["<capitalize(pcfg.currentRoot.file)>:<removeSpaces(link)>"]) { + u = /^\/assets/ := u ? u : "<p2r><u>"; + return compileMarkdown(["<prefix>[<addSpaces(link)>](<u>)<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + else if ({u} := ind["<capitalize(pcfg.currentRoot.file)>-<removeSpaces(link)>"]) { + u = /^\/assets/ := u ? u : "<p2r><u>"; + return compileMarkdown(["<prefix>[<addSpaces(link)>](<u>)<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + // or we check if its one of the details of the current concept + else if ({u} := ind["<capitalize(pcfg.currentRoot.file)>:<capitalize(pcfg.currentFile[extension=""].file)>-<removeSpaces(link)>"]) { + u = /^\/assets/ := u ? u : "<p2r><u>"; + return compileMarkdown(["<prefix>[<addSpaces(link)>](<u>)<postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position); + } + + return [ + err(error("Ambiguous concept link: <removeSpaces(link)> resolves to all of these: <for (r <- resolution) {><r> <}>", pcfg.currentFile(offset, 1, <line,0>,<line,1>), + cause="Please choose from the following options to disambiguate: <for (<str k, str v> <- rangeR(ind, ind[removeSpaces(link)]), {_} := ind[k]) {> + ' <k> resolves to <v><}>")), + *compileMarkdown(["<prefix> **broken:<link> (ambiguous)** <postfix>", *rest], line, offset, pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + } + } + + return [err(error("Unexpected state of link resolution for <link>: <resolution>", pcfg.currentFile(offset, 1, <line,0>,<line,1>)))]; +} + +@synopsis{extract what's needed from the header and print it back, also set sidebar_position} +list[Output] compileMarkdown([a:/^\-\-\-\s*$/, *str header, b:/^\-\-\-\s*$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) { + try { + model = unsetRec(loadYAML(trim(intercalate("\n", header)))); + dtls = [dtl | mapping(m) := model, scalar(str dtl) <- (m[scalar("details")]?sequence([])).\list]; + + if (dtls == []) { + dtls = createDetailsList(pcfg.currentFile, pcfg); + } + + return [ + details(dtls), + out("---"), + *[out(l) | l <- header], + *[out("sidebar_position: <sidebar_position>") | sidebar_position != -1], + out("---"), + out("\<div class=\"theme-doc-version-badge badge badge--secondary\"\>rascal-<getRascalVersion()>\</div\><if (pcfg.isPackageCourse) {> \<div class=\"theme-doc-version-badge badge badge--secondary\"\><pcfg.packageName>-<pcfg.packageVersion>\</div\><}>"), + out(""), + *compileMarkdown(rest, line + 2 + size(header), offset + size(a) + size(b) + length(header), pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + } + catch value e: { + switch(e) { + // case IllegalTypeArgument(str x, str y) : e = "<x>, <y>"; + case IllegalArgument(value i) : e = "<i>"; + case IO(str msg) : e = "<msg>"; + case Java(str class, str msg) : e = "<class>: <msg>"; + case Java(str class, str msg, value cause) : e = "<class>: <msg>, caused by: <cause>"; + } + + return [ + err(error("Could not process YAML header: <e>", pcfg.currentFile)), + out("---"), + *[out(l) | l <- header], + out("---"), + *compileMarkdown(rest, line + 2 + size(header), offset + size(a) + size(b) + length(header), pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + } +} + +@synopsis{Removes empty sections in the middle of a document} +list[Output] compileMarkdown([str first:/^\s*#+\s+<title:.*>$/, *str emptySection, nextSection:/^\s*#+\s+.*$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = compileMarkdown([nextSection, *rest], line + 1 + size(emptySection), offset + size(first) + length(emptySection), pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + when !(/\S/ <- emptySection); + +@synopsis{Divide the work over sections to avoid stackoverflows} +list[Output] compileMarkdown([str first:/^\s*#+\s+<title:.*>$/, *str body, nextSection:/^\s*#+\s+.*$/, *str rest], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = [ + *compileMarkdown([first, *body], line + 1, offset + length(first) + 1, pcfg, exec, ind, dtls, sidebar_position=sidebar_position), + *compileMarkdown([nextSection, *rest], line + 1 + size(body), offset + length(first) + 1 + length(body), pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ] when /\S/ <- body; + +@synopsis{Removes empty sections at the end of a document} +list[Output] compileMarkdown([str first:/^\s*#+\s+<title:.*>$/, *str emptySection, /^\s*$/], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = [] when !(/\S/ <- emptySection); + +@synopsis{this is when we have processed all the input lines} +list[Output] compileMarkdown([], int _/*line*/, int _/*offset*/, PathConfig _, CommandExecutor _, Index _, list[str] _, int sidebar_position=-1) = []; + +@synopsis{all other lines are simply copied to the output stream} +default list[Output] compileMarkdown([str head, *str tail], int line, int offset, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, int sidebar_position=-1) + = [ + out(head), + *compileMarkdown(tail, line + 1, offset + size(head) + 1, pcfg, exec, ind, dtls, sidebar_position=sidebar_position) + ]; + +list[Output] compileRascalShell(list[str] block, bool allowErrors, bool isContinued, int lineOffset, int offset, PathConfig pcfg, CommandExecutor exec, Index _) { + if (!isContinued) { + exec.reset(); + } + + errorsDetected = false; + lineOffsetHere = 0; + list[Output] result = []; + + result = OUT:for (str line <- block) { + if (/^\s*\/\/<comment:.*>$/ := line) { // comment line + append OUT : out("```"); + append OUT : out(trim(comment)); + append OUT : out("```rascal-shell"); + continue OUT; + } + append out("<exec.prompt()><line>"); + + output = exec.eval(line); + str result = output["text/plain"]?""; + str stderr = output["application/rascal+stderr"]?""; + str stdout = output["application/rascal+stdout"]?""; + str shot = output["application/rascal+screenshot"]?""; + str png = output["image/png"]?""; + + if (filterErrors(stderr) != "" && /cancelled/ !:= stderr) { + for (allowErrors, str errLine <- split("\n", stderr)) { + errorsDetected = true; + append OUT : out(errLine); + } + + if (!allowErrors) { + append OUT : err(error("Code execution failed: + ' <stderr>", pcfg.currentFile(offset, 1, <lineOffset + lineOffsetHere, 0>, <lineOffset + lineOffsetHere, 1>), cause=stderr)); + append OUT : out("```"); + append OUT : out(":::danger"); + append OUT : out("Rascal code execution failed (unexpectedly) during compilation of this documentation."); + append OUT : out(":::"); + append OUT : out("```rascal-shell"); + for (errLine <- split("\n", stderr)) { + append OUT : out(errLine); + } + append OUT : out("```"); + } + } + + if (stdout != "") { + for (outLine <- split("\n", stdout)[..500]) { + append OUT : out("<outLine>"); + } + } + + if (shot != "") { + loc targetFile = pcfg.bin + "assets" + capitalize(pcfg.currentRoot.file) + relativize(pcfg.currentRoot, pcfg.currentFile)[extension=""].path; + targetFile.file = targetFile.file + "_screenshot_<lineOffsetHere+lineOffset>.png"; + println("screenshot <targetFile>"); + writeBase64(targetFile, shot); + append OUT: out("```"); + append OUT: out("![image](<relativize(pcfg.bin, targetFile).path>)"); + append OUT: out("```rascal-shell"); + } + else if (result != "") { + for (str resultLine <- split("\n", result)) { + append OUT : out(resultLine); + } + } + + lineOffsetHere +=1; + } + + if (allowErrors && !errorsDetected) { + result += [ + out(":::warning"), + out("The above code block was declared to expect errors, but no errors were detected during its execution."), + out(":::"), + err(error("Code execution failed to produce an expected error", pcfg.currentFile(offset, 1, <lineOffset + lineOffsetHere, 0>, <lineOffset + lineOffsetHere, 1>))) + ]; + } + + return result; +} + +@synopsis{Prepare blocks run the REPL but show no input or output} +list[Output] compileRascalShellPrepare(list[str] block, bool isContinued, int lineOffset, int offset, PathConfig pcfg, CommandExecutor exec, Index _) { + if (!isContinued) { + exec.reset(); + } + + lineOffsetHere = 0; + + return OUT:for (str line <- block) { + output = exec.eval(line); + result = output["text/plain"]?""; + stderr = output["application/rascal+stderr"]?""; + stdout = output["application/rascal+stdout"]?""; + html = output["text/html"]?""; + str shot = output["application/rascal+screenshot"]?""; + str png = output["image/png"]?""; + + if (filterErrors(stderr) != "" && /cancelled/ !:= stderr) { + for (errLine <- split("\n", stderr)) { + append OUT : out(errLine); + } + + append out(":::danger"); + append OUT : out("Rascal code execution failed (unexpectedly) during compilation of this documentation."); + append OUT : out("\<pre\>"); + for (errLine <- split("\n", stderr)) { + append OUT : out(errLine); + } + append OUT : out("\</pre\>"); + append OUT : err(error("Code execution failed in prepare block: + ' <stderr>", pcfg.currentFile(offset, 1, <lineOffset + lineOffsetHere, 0>, <lineOffset + lineOffsetHere, 1>), cause=stderr)); + } + + if (shot != "") { + loc targetFile = pcfg.bin + "assets" + capitalize(pcfg.currentRoot.file) + relativize(pcfg.currentRoot, pcfg.currentFile)[extension=""].path; + targetFile.file = targetFile.file + "_screenshot_<lineOffsetHere+lineOffset>.png"; + println("screenshot <targetFile>"); + writeBase64(targetFile, shot); + append OUT: out("![image](<relativize(pcfg.bin, targetFile).path>)"); + } + + lineOffsetHere +=1; + } +} + +list[str] skipEmpty([/^s*$/, *str rest]) = skipEmpty(rest); +default list[str] skipEmpty(list[str] lst) = lst; + +private str filterErrors(str errorStream) = intercalate("\n", filterErrors(split("\n", errorStream))); + +private list[str] filterErrors([/^warning, ambiguity/, *str rest]) = filterErrors(rest); +private list[str] filterErrors([/^Generating parser/, *str rest]) = filterErrors(rest); +private default list[str] filterErrors([str head, *str tail]) = [head, *filterErrors(tail)]; +private list[str] filterErrors([]) = []; + +private int length(list[str] lines) = (0 | it + size(l) | str l <- lines); +private int length(str line) = size(line); + +private map[str, str] subscripts + = ( + "0" : "\u2080", + "1" : "\u2081", + "2" : "\u2082", + "3" : "\u2083", + "4" : "\u2084", + "5" : "\u2085", + "6" : "\u2086", + "7" : "\u2087", + "8" : "\u2088", + "9" : "\u2089", + "+" : "\u208A", + "-" : "\u208B", + "(" : "\u208C", + ")" : "\u208D", + "a" : "\u2090", + "e" : "\u2091", + "h" : "\u2095", + "i" : "\u1d62", + "j" : "\u2c7c", + "k" : "\u2096", + "l" : "\u2097", + "m" : "\u2098", + "n" : "\u2099", + "o" : "\u2092", + "p" : "\u209a", + "r" : "\u1d63", + "s" : "\u209b", + "t" : "\u209c", + "u" : "\u1d64", + "v" : "\u1d65", + "x" : "\u2093", + "A" : "\u2090", + "E" : "\u2091", + "H" : "\u2095", + "I" : "\u1d62", + "J" : "\u2c7c", + "K" : "\u2096", + "L" : "\u2097", + "M" : "\u2098", + "N" : "\u2099", + "O" : "\u2092", + "P" : "\u209a", + "R" : "\u1d63", + "S" : "\u209b", + "T" : "\u209c", + "U" : "\u1d64", + "V" : "\u1d65", + "X" : "\u2093" + ); + diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/Includer.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/Includer.rsc new file mode 100644 index 00000000000..d62904bf1c6 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/Includer.rsc @@ -0,0 +1,49 @@ + +@bootstrapParser +module lang::rascal::tutor::Includer + +import lang::rascal::\syntax::Rascal; +import lang::rascal::tutor::Output; +import lang::rascal::tutor::Names; +import util::Reflective; +import String; +import Message; + + +list[Output] prepareModuleForInclusion(str moduleName, bool includeHeaders, bool includeTests, PathConfig pcfg) { + try { + moduleLoc = getModuleLocation(trim(moduleName), pcfg); + start[Module] moduleTree = parseModuleWithSpaces(moduleLoc); + + if (!includeTests) { + moduleTree = visit(moduleTree) { + case (Module) `<Header h> <Body toplevels>` => (Module) `<Header h> + ' + '<Body filteredToplevels>` + when filteredToplevels:= removeTests(toplevels) + } + } + + if (!includeHeaders) { + moduleTree = visit(moduleTree) { + // TODO: this filters tags of everything, not just the top. + case Tags _ => (Tags) `` + } + } + + return [out(l) | str l <- split("\n", "<moduleTree>")]; + } + catch str notFound: { + return [err(error(notFound, pcfg.currentFile))]; + } + catch ParseError(loc f): { + return [err(error("parse error in included module", f))]; + } +} + +Body removeTests((Body) `<Toplevel* begin> <Toplevel elem> <Toplevel* end>`) + = (Body) `<Toplevel* begin> + '<Toplevel* end>` when /(FunctionModifier) `test` := elem + ; + +default Body removeTests(Body b) = b; \ No newline at end of file diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/Indexer.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/Indexer.rsc new file mode 100644 index 00000000000..ec44be94c4b --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/Indexer.rsc @@ -0,0 +1,248 @@ +module lang::rascal::tutor::Indexer + +import util::Reflective; +import ValueIO; +import String; +import util::FileSystem; +import util::Monitor; +import IO; +import ValueIO; +import Location; + +import lang::rascal::tutor::apidoc::DeclarationInfo; +import lang::rascal::tutor::apidoc::ExtractInfo; +import lang::rascal::tutor::Names; +// import Relation; +import Location; +import Set; + +public alias Index = rel[str reference, str url]; + +Index readConceptIndex(PathConfig pcfg) { + return readBinaryValueFile(#Index, pcfg.bin + "index.value"); +} + +Index createConceptIndex(PathConfig pcfg) { + targetFile = pcfg.bin + "index.value"; + + // in incremental mode we will have skipped many files. This + // adds the old index to the newly created ones + ind = exists(targetFile) ? readConceptIndex(pcfg) : {}; + + // now we add the new index items on top of the old ones + ind += createConceptIndex(pcfg.srcs, exists(targetFile) ? lastModified(targetFile) : $1970-01-01T00:00:00.000+00:00$, pcfg.isPackageCourse, pcfg.packageName); + + // store index for later usage by depending documentation projects, + // and for future runs of the compiler on the current project + writeBinaryValueFile(targetFile, ind); + + // read indices from projects we depend on, if present + ind += {*readBinaryValueFile(#rel[str,str], inx) | l <- pcfg.libs, inx := l + "docs" + "index.value", exists(inx)}; + + return ind; +} + +rel[str, str] createConceptIndex(list[loc] srcs, datetime lastModified, bool isPackageCourse, str packageName) + = {*createConceptIndex(src, lastModified, isPackageCourse, packageName) | src <- srcs, bprintln("Indexing <src>")}; + +@synopsis{creates a lookup table for concepts nested in a folder} +rel[str, str] createConceptIndex(loc src, datetime lastModified, bool isPackageCourse, str packageName) { + bool step(str label, loc file) { + jobStep(label, "<file.file>"); + return true; + } + + void \start(str label, int work) { + if (work > 0) { + jobStart(label, totalWork=work); + } + } + + // first we collect index entries for concept names, each file is one concept which + // can be linked to in many different ways ranging from very short (handy but inexact) to very long (guaranteed to be exact.) + conceptFiles = find(src, isFreshConceptFile(lastModified)); + \start("Indexing concepts", 2*size(conceptFiles)); + + imageFiles = find(src, isImageFile); + \start("Indexing images", size(imageFiles)); + + directoryIndexes = find(src, isDirectory); + \start("Indexing directories", 2*size(directoryIndexes)); + + rascalFiles = find(src, isFreshRascalFile(lastModified)); + \start("Indexing modules", size(rascalFiles)); + + // First we handle the root concept + result = { + <RootName, "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<RootName>/index.md">, + <"course:<RootName>", "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<RootName>/index.md"> + | str RootName := ((isPackageCourse && src.file in {"src","rascal","api"}) ? "API" : package(src.file)) + } + + + // Then we handle the cases where the concept name is the same as the folder it is nested in: + { + // `((StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <cf.file , fr>, + + // `((Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<f.parent.parent.file>-<cf.file>", fr>, + + // `((Expressions-Values-Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <replaceAll(capitalize(relativize(src, f.parent).path)[1..], "/", "-"), fr>, + + // `((Rascal:StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<capitalize(src.file)>:<capitalize(cf.file)>", fr>, + + // `((Rascal:Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<capitalize(src.file)>:<f.parent.parent.file>-<cf.file>", fr>, + + // `((Rascal:Expressions-Values-Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<capitalize(src.file)>:<replaceAll(capitalize(relativize(src, f.parent).path)[1..], "/", "-")>", fr> + + | loc f <- conceptFiles + , step("Indexing concepts", f) + , f.parent? + , f.parent.path != "/" + , f.parent != src + , f.parent.file == f[extension=""].file + , fr := "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>API<} else {><capitalize(src.file)><}>/<fragment(src, f)>" + , cf := f[extension=""] + } + + + // Then we handle the extra markdown files, that don't keep to the Concept/Concept.md rule + { + // `((StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <cf.file , fr>, + + // `((Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<f.parent.file>-<cf.file>", fr>, + + // `((Expressions-Values-Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <replaceAll(capitalize(relativize(src, cf).path)[1..], "/", "-"), fr>, + + // `((Rascal:StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<capitalize(src.file)>:<capitalize(cf.file)>", fr>, + + // `((Rascal:Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<capitalize(src.file)>:<f.parent.file>-<cf.file>", fr>, + + // `((Rascal:Expressions-Values-Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<capitalize(src.file)>:<replaceAll(capitalize(relativize(src, cf).path)[1..], "/", "-")>", fr> + + | loc f <- conceptFiles + , step("Indexing concepts", f) + , f.parent? + , f.parent.path != "/" + , f.parent != src + , f.parent.file != f[extension=""].file + , fr := "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>API<} else {><capitalize(src.file)><}>/<fragment(src, f)>" + , cf := f[extension=""] + } + // Then we handle all folders. We assume all folders have an index.md (generated or manually provided) + // This may generate some links exactly the same as above, and add some new ones. + + + { + // `((StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <f.file , fr>, + + // `((Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<f.parent.file>-<f.file>", fr>, + + // `((Expressions-Values-Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <replaceAll(capitalize(relativize(src, f).path)[1..], "/", "-"), fr>, + + // `((Rascal:StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<capitalize(src.file)>:<capitalize(f.file)>", fr>, + + // `((Rascal:Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<capitalize(src.file)>:<f.parent.file>-<f.file>", fr>, + + // `((Rascal:Expressions-Values-Set-StrictSuperSet)) -> /Rascal/Expressions/Values/Set/StrictSuperSet/index.md` + <"<capitalize(src.file)>:<replaceAll(capitalize(relativize(src, f).path)[1..], "/", "-")>", fr> + | loc f <- directoryIndexes + , step("Indexing directories", f) + , fr := "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>API<} else {><capitalize(src.file)><}>/<fragment(src, f)>" + , f != src + } + + + // Now follow the index entries for image files: + { <"<f.parent.file>-<f.file>", fr>, + <f.file, fr>, + <"<capitalize(src.file)>:<f.file>", fr> + | loc f <- imageFiles, step("Indexing images", f), + fr := "/assets/<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>API<} else {><capitalize(src.file)><}><relativize(src, f).path>" + } + + { // these are links to packages/folders/directories via module path prefixes, like `analysis::m3` + <"<replaceAll(relativize(src, f).path[1..], "/", "::")>", fr>, + <"<capitalize(src.file)>:<replaceAll(relativize(src, f).path[1..], "/", "::")>", fr>, + <"<capitalize(src.file)>:<replaceAll(relativize(src, f).path[1..], "/", "-")>", fr>, + <"<capitalize(src.file)>:<capitalize(replaceAll(relativize(src, f).path[1..], "/", "-"))>", fr>, + <"<capitalize(src.file)>:package:<replaceAll(relativize(src, f).path[1..], "/", "::")>", fr>, + <"<capitalize(src.file)>:<capitalize(replaceAll(relativize(src, f).path[1..], "/", "::"))>", fr> + | loc f <- directoryIndexes + , step("Indexing directories", f) + , /\/internal\// !:= f.path + , f != src + , fr := "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>API<} else {><capitalize(src.file)><}>/<fragment(src, f)>" + } + + // Finally, the index entries for Rascal modules and declarations, as extracted from the source code: + { // `((getDefaultPathConfig))` -> `Libary/util/Reflective#getDefaultPathConfig` + *{<"<item.kind>:<item.name>", fr>, + <item.name, fr > | item.name?}, + + // `((Library:getDefaultPathConfig))` -> `/Library/util/Reflective#getDefaultPathConfig` + *{<"<capitalize(src.file)>:<item.name>", fr >, + <"<capitalize(src.file)>:<item.kind>:<item.name>", fr > | item.name?, !(item is moduleInfo)}, + + // `((util::Reflective::getDefaultPathConfig))` -> `/Library/util/Reflective#getDefaultPathConfig` + *{<"<item.moduleName><sep><item.name>", fr >, + <"<item.kind>:<item.moduleName><sep><item.name>", fr > | item.name?, !(item is moduleInfo), sep <- {"::", "/", "-"}}, + + // ((Library:util::Reflective::getDefaultPathConfig))` -> `/Library/util/Reflective#getDefaultPathConfig` + *{<"<capitalize(src.file)>:<item.moduleName><sep><item.name>", fr >, + <"<capitalize(src.file)>:<item.kind>:<item.moduleName><sep><item.name>", fr > | item.name?, !(item is moduleInfo), sep <- {"::", "/", "-"}}, + + // ((Set)) -> `/Library/Set` + *{<item.moduleName, "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>/API<} else {>/<capitalize(src.file)><}>/<modulePath(item.moduleName)>.md" >, + <"module:<item.moduleName>", "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>/API<} else {>/<capitalize(src.file)><}>/<modulePath(item.moduleName)>.md" > + | item is moduleInfo + }, + + // `((Library:Set))` -> `/Library/Set` + *{<"<capitalize(src.file)>:<item.moduleName>", "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>/API<} else {>/<capitalize(src.file)><}>/<modulePath(item.moduleName)>.md" >, + <"<capitalize(src.file)>:module:<item.moduleName>", "<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>/API<} else {>/<capitalize(src.file)><}>/<modulePath(item.moduleName)>.md" > | item is moduleInfo} + + | loc f <- rascalFiles, step("Indexing modules", f), list[DeclarationInfo] inf := safeExtract(f), item <- inf, + fr := "/<if (isPackageCourse) {>/Packages/<package(packageName)><}>/<if (isPackageCourse && src.file in {"src","rascal","api"}) {>API<} else {><capitalize(src.file)><}>/<modulePath(item.moduleName)>.md<moduleFragment(item.moduleName)>-<item.name>" + }; + + jobEnd("Indexing modules"); + jobEnd("Indexing directories"); + jobEnd("Indexing concepts"); + jobEnd("Indexing images"); + + return result; +} + +private bool isConceptFile(loc f) = f.extension in {"md"}; + +private bool(loc) isFreshConceptFile(datetime lM) + = bool (loc f) { + return isConceptFile(f) && lastModified(f) > lM; + }; + +private bool(loc) isFreshRascalFile(datetime lM) + = bool (loc f) { + return f.extension in {"rsc"} && lastModified(f) > lM; + }; + +private bool isImageFile(loc f) = f.extension in {"png", "jpg", "svg", "jpeg"}; + +@synopsis{ignores extracting errors because they will be found later} +private list[DeclarationInfo] safeExtract(loc f) { + try { + return extractInfo(f); + } + catch Java(_,_): return []; + catch ParseError(_): return []; +} \ No newline at end of file diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/Names.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/Names.rsc new file mode 100644 index 00000000000..5f57c13efc5 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/Names.rsc @@ -0,0 +1,69 @@ +module lang::rascal::tutor::Names + +import String; +import Location; +import List; +import IO; +import util::Reflective; + +data PathConfig( + str packageName="", + str packageGroup="", + loc packageRoot=|unknown:///|, + loc sources=|http://github.com/usethesource/rascal|, + loc issues=|http://github.com/usethesource/rascal/issues|, + loc license=|cwd:///LICENSE.md|, + loc citation=|cwd:///CITATION.md|, + loc funding=|cwd:///FUNDING.md|, + loc releaseNotes=|cwd:///RELEASE-NOTES.md|, + str packageVersion=getRascalVersion(), + bool isPackageCourse=false +); + +data PathConfig(loc currentRoot = |unknown:///|, loc currentFile = |unknown:///|); +data Message(str cause=""); + +default str fragment(loc root, loc concept) = capitalize(relativize(root, concept).path)[1..]; + +str fragment(loc root, loc concept) = fragment(root, concept + "index.md") + when isDirectory(concept) || root == concept; + +str fragment(loc root, loc concept) = fragment(root, concept.parent + "index.md") + when concept.parent?, concept.parent.file == concept[extension=""].file; + +str modulePath(/^<prefix:.*>::Index$/) = modulePath("<prefix>::module_Index"); +default str modulePath(str moduleName) = "<replaceAll(moduleName, "::", "/")>"; +default str moduleFragment(str moduleName) = "#<replaceAll(moduleName, "::", "-")>"; + +@synopsis{capitalizes and removes hyphens} +default str package(str input) = input; +str package(str input:/^[a-z].*$/) = package(capitalize(input)); +str package(/^<prefix:[a-zA-Z\_0-9]*>\-<rest:.*>$/) = package("<prefix><capitalize(rest)>"); + +str removeSpaces(/^<prefix:.*><spaces:\s+><postfix:.*>$/) + = removeSpaces("<prefix><capitalize(postfix)>"); + +default str removeSpaces(str s) = s; + +// remove Course:module: prefixes +str addSpaces(/^<prefix:[^:]+>:<postfix:[^:].*>$/) + = addSpaces(postfix); + +// select final function name if present +str addSpaces(/^<prefix:.+>::<name:[^:]+>$/) + = name; // no recursion to avoid splitting function names + +// split and uncapitalize CamelCase +str addSpaces(/^<prefix:[A-Za-z0-9\ ]+[a-z0-9]><postfix:[A-Z].+>/) = + addSpaces("<uncapitalize(prefix)> <uncapitalize(postfix)>"); + +default str addSpaces(str s) = capitalize(split("-", s)[-1]); + +@synopsis{produces `"../../.."` for pathToRoot(|aap:///a/b|, |aap:///a/b/c/d|)} +str pathToRoot(loc root, loc src, bool isPackageCourse) + = "<if (isPackageCourse) {>../../<}>..<for (e <- split("/", relativize(root, src).path), e != "") {>/..<}>" + when isDirectory(src); + +str pathToRoot(loc root, loc src, bool isPackageCourse) + = pathToRoot(root, src.parent, isPackageCourse) + when isFile(src); diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/Output.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/Output.rsc new file mode 100644 index 00000000000..b33d91bd2fd --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/Output.rsc @@ -0,0 +1,40 @@ +module lang::rascal::tutor::Output + +extend Message; +import ParseTree; +import String; + +data Output + = line(str content) + | err(Message message) + | details(list[str] order) + | search(list[str] contents, str fragment) + | \docTag(str tagName, list[Output] output) + ; + + +@synopsis{multi line comment with a lonely callout} +Output out(/<pre:.*><t:\/\*\s*\<\s*[0-9]*\s*\>\s*\*\/><post:.*>/) = out("<pre> <callout(t)> <post>"); + +@synopsis{single line comment with a lonely callout} +Output out(/^<pre:.*><t:\/\/\s*\<\s*[0-9]+\s*\>><post:\s*>$/) = out("<pre> <callout(t)> <post>"); + +@synopsis{bullets with callouts} +Output out(/^<pre:\s*\*>\s+<t:\<\s*[0-9]+\s*\>><post:.*>$/) = out("<pre><callout(trim(t))><post>"); + +@synopsis{callouts as bullets} +Output out(/^<pre:\s*><t:\<\s*[0-9]+\s*\>><post:.*>$/) = out("<pre>*<callout(trim(t))><post>"); + +default Output out(str output) = line(output); + +Output empty() = line(""); + + +@synopsis{replace all characters by space, except the digits by callout digits} +str callout(str input) = visit(input) { + case /^<ix:[0-9]>/ => callout(toInt(ix)) + case /^./ => " " +}; + +str callout(0) = "⓿"; +str callout(int i) = "<char(0x2775 + i)>" when i >= 1 && i <= 9; diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/apidoc/DeclarationInfo.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/apidoc/DeclarationInfo.rsc new file mode 100644 index 00000000000..d3870d97ef9 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/apidoc/DeclarationInfo.rsc @@ -0,0 +1,24 @@ +module lang::rascal::tutor::apidoc::DeclarationInfo + +@doc{Representation of documentation-related information extracted from a module.} +data DeclarationInfo( + str moduleName="", + str name=moduleName, + loc src = |unknown:///|, + str synopsis="", + str signature="", + list[DocTag] docs = [], + loc docSrc = src) + = moduleInfo (str kind="module", bool demo=false, list[str] dependencies=[]) + | functionInfo (str kind="function", str fullFunction="") + | testInfo (str kind="test", str fullTest="") + | constructorInfo (str kind="constructor") + | dataInfo (str kind="data", list[str] overloads=[]) + | aliasInfo (str kind="alias") + | varInfo (str kind="variable") + | syntaxInfo (str kind="syntax") + ; + +data DocTag(str label="", loc src=|unknown:///|, str content="") + = docTag(); + diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/apidoc/ExtractInfo.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/apidoc/ExtractInfo.rsc new file mode 100644 index 00000000000..6835b14b45d --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/apidoc/ExtractInfo.rsc @@ -0,0 +1,229 @@ +@bootstrapParser +module lang::rascal::tutor::apidoc::ExtractInfo + +import IO; +import String; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import util::Reflective; +import DateTime; +import lang::rascal::tutor::apidoc::DeclarationInfo; + +@synopsis{Extract declaration information from a Rascal module at given location.} +list[DeclarationInfo] extractInfo(loc moduleLoc) + = doExtractInfo(moduleLoc, lastModified(moduleLoc)); + +@memo +private list[DeclarationInfo] doExtractInfo(loc moduleLoc, datetime _/*lastModified*/){ + M = parseModuleWithSpaces(moduleLoc).top; + return extractModule(M); +} + +list[DeclarationInfo] extractModule(m: (Module) `<Header header> <Body body>`) { + moduleName = "<header.name>"; + tags = getTagContents(header.tags); + name = "<header.name.names[-1]>"; + + if (name == "Index") { + name = "module_Index"; + } + + tls = [*extractImport(moduleName, imp) | imp <- header.imports] + + [*extractTopLevel(moduleName, tl) | tl <- body.toplevels ]; + + synopsis = getSynopsis(tags); + + return [moduleInfo( + moduleName=moduleName, + name=name, + src=m@\loc, + synopsis=synopsis, + docs=sortedDocTags(tags), + demo=(/demo|examples/ := moduleName), + dependencies=[trim("<d>") | d <- header.imports, !(d is \syntax)] + )] + tls; +} + +/********************************************************************/ +/* Process imports and syntax definitions */ +/********************************************************************/ + +default list[DeclarationInfo] extractImport(str moduleName, (Import) `<SyntaxDefinition def>` ) + = [syntaxInfo(moduleName=moduleName, name="<def.defined>", signature="<def>")]; + +default list[DeclarationInfo] extractImport(str _moduleName, Import _ ) = []; + +/********************************************************************/ +/* Process declarations in a module */ +/********************************************************************/ + +list[DeclarationInfo] extractTopLevel(str moduleName, (Toplevel) `<Declaration decl>`) = extractDecl(moduleName, decl); + +// -- variable declaration ------------------------------------------ + +list[DeclarationInfo] extractDecl(str moduleName, d: (Declaration) `<Tags tags> <Visibility visibility> <Type tp> <{Variable ","}+ variables> ;`) + = []; + +// -- miscellaneous declarations ------------------------------------ + +list[DeclarationInfo] extractDecl(str moduleName, d: (Declaration) `<Tags tags> <Visibility visibility> anno <Type annoType> <Type onType>@<Name name> ;`) + = []; + +list[DeclarationInfo] extractDecl(str moduleName, d: (Declaration) `<Tags tags> <Visibility visibility> alias <UserType user> = <Type base> ;`) { + dtags = getTagContents(tags); + return [ aliasInfo(moduleName=moduleName, name="<user.name>", signature="<base>", src=d@\loc, synopsis=getSynopsis(dtags), docs=sortedDocTags(dtags))]; +} + +list[DeclarationInfo] extractDecl(str moduleName, d: (Declaration) `<Tags tags> <Visibility visibility> tag <Kind kind> <Name name> on <{Type ","}+ types> ;`) + = [ ]; + +list[DeclarationInfo] extractDecl(str moduleName, d: (Declaration) `<Tags tags> <Visibility visibility> data <UserType user> ;`) + = [ ]; + +str align({Variant "|"}+ variants){ + res = ""; + sep = "\n = "; + for(v <- variants){ + res += sep + trim("<v>"); + sep = "\n | "; + } + return res + "\n ;"; +} + +list[DeclarationInfo] extractDecl(str moduleName, d: (Declaration) `<Tags tags> <Visibility visibility> data <UserType user> <CommonKeywordParameters commonKeywordParameters> ;`) { + dtags = getTagContents(tags); + adtName = "<user.name>"; + + return [dataInfo(moduleName=moduleName, name=adtName, signature="data <user> <commonKeywordParameters>", + src=d@\loc, synopsis=getSynopsis(dtags), docs=sortedDocTags(dtags))]; +} + +list[DeclarationInfo] extractDecl(str moduleName, d: (Declaration) `<Tags tags> <Visibility visibility> data <UserType user> <CommonKeywordParameters commonKeywordParameters> = <{Variant "|"}+ variants> ;`) { + dtags = getTagContents(tags); + adtName = "<user.name>"; + + infoVariants = [ genVariant(moduleName, variant) | variant <- variants ]; + + return dataInfo(moduleName=moduleName, name=adtName, signature="data <user> <commonKeywordParameters> <align(variants)>", + src=d@\loc, synopsis=getSynopsis(dtags), docs=sortedDocTags(dtags)) + infoVariants; +} + +DeclarationInfo genVariant(str moduleName, v: (Variant) `<Name name>(<{TypeArg ","}* _> <KeywordFormals _>)`) { + signature = "<v>"; + return constructorInfo(moduleName=moduleName, name="<name>", signature="<v>", src=v@\loc); +} + +list[DeclarationInfo] extractDecl(str moduleName, d: (Declaration) `<FunctionDeclaration functionDeclaration>`) + = [ extractTestDecl(moduleName, functionDeclaration) ] when /FunctionModifier m := functionDeclaration.signature, (FunctionModifier) `test` := m; + +default list[DeclarationInfo] extractDecl(str moduleName, d: (Declaration) `<FunctionDeclaration functionDeclaration>`) + = [ extractFunDecl(moduleName, functionDeclaration) ]; + +// -- function declaration ------------------------------------------ + +DeclarationInfo extractFunDecl(str moduleName, fd: (FunctionDeclaration) `<Tags tags> <Visibility visibility> <Signature signature> ;`) + = extractFunctionDeclaration(moduleName, fd); + +DeclarationInfo extractFunDecl(str moduleName, fd: (FunctionDeclaration) `<Tags tags> <Visibility visibility> <Signature signature> = <Expression expression> ;`) + = extractFunctionDeclaration(moduleName, fd); + +DeclarationInfo extractFunDecl(str moduleName, fd: (FunctionDeclaration) `<Tags tags> <Visibility visibility> <Signature signature> = <Expression expression> when <{Expression ","}+ conditions>;`) + = extractFunctionDeclaration(moduleName, fd); + + +DeclarationInfo extractFunDecl(str moduleName, fd: (FunctionDeclaration) `<Tags tags> <Visibility visibility> <Signature signature> <FunctionBody body>`) + = extractFunctionDeclaration(moduleName, fd); + +private DeclarationInfo extractFunctionDeclaration(str moduleName, FunctionDeclaration fd) { + fname = "<fd.signature.name>"; + + signature = "<fd.signature>"; + if(startsWith(signature, "java")){ + signature = signature[size("java")+1 .. ]; + } + + tags = getTagContents(fd.tags); + + return functionInfo(moduleName=moduleName, name=fname, signature=signature, src=fd@\loc, synopsis=getSynopsis(tags), docs=sortedDocTags(tags), fullFunction="<removeTags(fd)>"); +} + +DeclarationInfo extractTestDecl(str moduleName, FunctionDeclaration fd) { + fname = "<fd.signature.name>"; + + signature = "<fd.signature>"; + tags = getTagContents(fd.tags); + + return testInfo(moduleName=moduleName, name=fname, src=fd@\loc, synopsis=getSynopsis(tags), fullTest="<removeTags(fd)>"); +} + +private Tree removeTags(Tree x) = visit(x) { + case Tags _ => (Tags) `` +}; + +str getSynopsis(rel[str, DocTag] tags) { + if (docTag(content=str docContents) <- tags["doc"]) { + if ([*_, /^.Synopsis\s+<rest:.*>$/, *str cont, /^.[A-Za-z].*$/, *str _] := split("\n", docContents)) { + return intercalate(" ", [rest, *cont]); + } + else if ([*_, /^#+\s*Synopsis\s+<rest:.*>$/, *str cont, /^.[A-Za-z].*$/, *str _] := split("\n", docContents)) { + return intercalate(" ", [rest, *cont]); + } + } + + if (docTag(content=str docContents) <- tags["synopsis"]) { + if (docTag(content=str deprMessage) <- tags["deprecated"]) { + return "<trim(intercalate(" ", split("\n", docContents)))> + ' + ':::warning + '**deprecated: marked for future deletion** + '<deprMessage> + '::: + '"; + } + else { + return trim(intercalate(" ", split("\n", docContents))); + } + + } + else { + return ""; + } +} + + +bool isTutorTag(str label) = label in {"doc", "synopsis", "syntax", "types", "details", "description", "examples", "benefits", "pitfalls", "deprecated"}; + +@synopsis{extracts the contents of _all_ tags from a declaration syntax tree and stores origin information} +rel[str, DocTag] getTagContents(Tags tags){ + m = {}; + for (tg <- tags.tags){ + str name = "<tg.name>"; + if (!isTutorTag(name)) { + continue; + } + + if (tg is \default) { + cont = "<tg.contents>"[1 .. -1]; + m += <name, docTag(label=name, content=cont, src=tg.src)>; + } else if (tg is empty) { + m += <name, docTag(label=name, content="", src=tg.src)>; + } else { + m += <name, docTag(label=name, content="<tg.expression>"[1 .. -1], src=tg.src)>; + } + } + + return m; +} + +@synopsis{lists the supported documentation tags in the prescribed order} +list[DocTag] sortedDocTags(rel[str, DocTag] tags) + = [ *tags["doc"], + *tags["synopsis"], + *tags["syntax"], + *tags["types"], + *tags["details"], + *tags["description"], + *tags["examples"], + *tags["benefits"], + *tags["pitfalls"] + ]; diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/apidoc/GenerateMarkdown.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/apidoc/GenerateMarkdown.rsc new file mode 100644 index 00000000000..7ada5224f57 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/apidoc/GenerateMarkdown.rsc @@ -0,0 +1,217 @@ +module lang::rascal::tutor::apidoc::GenerateMarkdown + +import List; +import String; +import util::Reflective; +import Location; +import Message; + +import lang::rascal::tutor::apidoc::DeclarationInfo; +import lang::rascal::tutor::apidoc::ExtractInfo; +import lang::rascal::tutor::Output; +import lang::rascal::tutor::Indexer; +import lang::rascal::tutor::Compiler; +import lang::rascal::tutor::repl::TutorCommandExecutor; +import lang::rascal::tutor::Names; +import IO; +import Node; + +@synopsis{Generate markdown documentation from the declarations extracted from a Rascal module.} +@description{ + This function takes Rascal files as input, first extracts all declarations including their + embedded (markdown) documentation tags, and then generates on-the-fly the output markdown + as a list of lines and error messages. + + This generator reuses the markdown compiler + to implement Rascal shell execution and concept linking, etc. This compilation is applied inside of the + documentation tags that are written by the author of the Rascal code. The trick is to track the + current line number inside those documentation tags to provide valuable feedback to the user + of the tutor compiler. +} +list[Output] generateAPIMarkdown(str parent, loc moduleLoc, PathConfig pcfg, CommandExecutor exec, Index ind) { + try { + dinfo = extractInfo(moduleLoc); + + // filter the tests + tests = [t | t:testInfo() <- dinfo]; + + isDemo = DeclarationInfo k <- dinfo && k is moduleInfo && k.demo; + + // remove the tests + dinfo -= tests; + + dtls = sort(dup(["<capitalize(pcfg.currentRoot.file)>:<i.kind>:<i.moduleName>::<i.name>" | DeclarationInfo i <- dinfo, !(i is moduleInfo)])); + + // TODO: this overloading collection should happen in ExtractInfo + res = []; + int i = 0; + while (i < size(dinfo)) { + int j = i + 1; + list[str] overloads = []; + + if (dinfo[i] has name) { + overloads = [(isDemo && dinfo[i].fullFunction?) ? dinfo[i].fullFunction : dinfo[i].signature]; + + // TODO: this only collects consecutive overloads. if a utility function interupts the flow, + // then we do not get to see the other overloads with the current group. Rewrite to use a "group-by" query. + // Also this looses any additional documentation tags for anything but the first overloaded declaration + + while (j < size(dinfo) && dinfo[i].name == dinfo[j].name && dinfo[j].synopsis=="" && getName(dinfo[i]) == getName(dinfo[j]) /* same kinds */) { + // this loops eats the other declarations with the same name (if consecutive!) + overloads += [((isDemo && dinfo[j].fullFunction?) ? dinfo[j].fullFunction : dinfo[j].signature)]; + j += 1; + } + } + + res += declInfo2Doc(parent, dinfo[i], overloads, pcfg, exec, ind, dinfo[i] is moduleInfo? dtls : [], isDemo); + i = j; + } + + if (tests != []) { + res += line("# Tests"); + } + + for (di <- tests) { + res += declInfo2Doc(parent, di, [], pcfg, exec, ind, [], isDemo); + } + + return res; + } + catch Java(_,_): + return [err(error("parse error in source file", moduleLoc))]; + catch ParseError(loc l): + return [err(error("parse error in source file", l))]; +} + +private map[str,str] escapes = ("\\": "\\\\", "\"": "\\\""); + +list[Output] declInfo2Doc(str parent, d:moduleInfo(), list[str] overloads, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, bool demo) = + [ + out("---"), + out("title: \"module <"<[d.moduleName]>"[2..-2]>\""), // we make sure to escape backslashes here (e.g. lang::pico::\syntax::Main) + out("id: <d.name>"), + out("slug: <parent>/<d.name>"), + out("---"), + Output::empty(), + out("\<div class=\"theme-doc-version-badge badge badge--secondary\"\>rascal-<getRascalVersion()>\</div\><if (pcfg.isPackageCourse) {> \<div class=\"theme-doc-version-badge badge badge--secondary\"\><pcfg.packageName>-<pcfg.packageVersion>\</div\><}>"), + Output::empty(), + *[out(synopsis.content) | synopsis:docTag(label="synopsis") <- d.docs], + out("#### Usage"), + Output::empty(), + out("```rascal"), + out("import <replaceAll(d.moduleName, "/", "::")>;"), + out("```"), + Output::empty(), + *[out("#### Source code"), + out("<(pcfg.sources + relativize(pcfg.packageRoot, pcfg.currentRoot).path) + relativize(pcfg.currentRoot, d.src).path>"[1..-1]), + Output::empty() | pcfg.isPackageCourse, pcfg.sources?, pcfg.packageRoot? + ], + *[ + out("#### Dependencies"), + out("```rascal"), + *[ out(dep) | dep <- d.dependencies], + out("```") + | d.dependencies != [] + ], + Output::empty(), + *tags2Markdown(d.docs, pcfg, exec, ind, dtls, demo, descriptionHeader=((pcfg.sources? && pcfg.packageRoot?) || d.dependencies != [])), + Output::empty() + ]; + +list[Output] declInfo2Doc(str parent, d:functionInfo(), list[str] overloads, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, bool demo) = + [ + out("## function <d.name> {<moduleFragment(d.moduleName)>-<d.name>}"), + *[Output::empty(), out(synopsis.content) | synopsis:docTag(label="synopsis") <- d.docs], + empty(), + out("```rascal"), + *([ *[out(defLine) | str defLine <- split("\n", ov)], empty() | ov <- overloads][..-1]), + out("```"), + Output::empty(), + *tags2Markdown(d.docs, pcfg, exec, ind, dtls, demo) + ]; + +list[Output] declInfo2Doc(str parent, d:testInfo(), list[str] overloads, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, bool demo) = + [ + out("## test <d.name> {<moduleFragment(d.moduleName)>-<d.name>}"), + *[Output::empty(), out(synopsis.content) | synopsis:docTag(label="synopsis") <- d.docs], + Output::empty(), + out("```rascal"), + *[out(defLine) | str defLine <- split("\n", d.fullTest)], + out("```"), + Output::empty(), + *tags2Markdown(d.docs, pcfg, exec, ind, dtls, demo) + ]; + + list[Output] declInfo2Doc(str parent, constructorInfo(), list[str] overloads, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, bool demo) = + []; + + list[Output] declInfo2Doc(str parent, d:dataInfo(), list[str] overloads, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, bool demo) = + [ + out("## data <d.name> {<moduleFragment(d.moduleName)>-<d.name>}"), + *[out(synopsis.content) | synopsis:docTag(label="synopsis") <- d.docs], + empty(), + *[ + out("```rascal"), + *[out(defLine) | str defLine <- split("\n", ov)], + out("```"), + empty() + | ov <- overloads + ], + *tags2Markdown(d.docs, pcfg, exec, ind, dtls, demo) + ]; + +list[Output] declInfo2Doc(str parent, d:syntaxInfo(), list[str] overloads, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, bool demo) = + [ + out("## syntax <d.name> {<moduleFragment(d.moduleName)>-<d.name>}"), + *[Output::empty(), out(synopsis.content) | synopsis:docTag(label="synopsis") <- d.docs], + empty(), + *[ + out("```rascal"), + *[out(defLine) | str defLine <- split("\n", ov)], + out("```"), + empty() + | ov := d.signature + ], + *tags2Markdown(d.docs, pcfg, exec, ind, dtls, demo) + ]; + +list[Output] declInfo2Doc(str parent, d:aliasInfo(), list[str] overloads, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, bool demo) = + [ + out("## alias <d.name> {<moduleFragment(d.moduleName)>-<d.name>}"), + *[Output::empty(), out(synopsis.content) | synopsis:docTag(label="synopsis") <- d.docs], + empty(), + out("```rascal"), + *[out(removeNewlines(ov)), empty() | ov <- overloads][..-1], + out("```"), + empty(), + *tags2Markdown(d.docs, pcfg, exec, ind, dtls, demo) + ]; + +default list[Output] declInfo2Doc(str parent, DeclarationInfo d, list[str] overloads, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, bool demo) + = [err(info("No content generated for <d>", d.src))]; + +list[Output] tags2Markdown(list[DocTag] tags, PathConfig pcfg, CommandExecutor exec, Index ind, list[str] dtls, bool _demo, bool descriptionHeader=false) + = [ + // every doc tag has its own header title, except the "doc" tag which may contain them all (backward compatibility) + // and description starts without a header to improver the ratio between content and structure in the documentation for smaller functions and modules + *(l notin {"doc", (!descriptionHeader) ? "description" : ""} ? [out("#### <capitalize(l)>"), empty()] : []), + + // here is where we get the origin information into the right place for error reporting: + *compileMarkdown(split("\n", c), s.begin.line, s.offset, pcfg, exec, ind, dtls), + + empty() + + // this assumes that the doc tags have been ordered correctly already by the extraction stage + | docTag(label=str l, src=s, content=str c) <- tags, l != "synopsis" + ]; + +public str basename(str cn){ + return (/^.*::<base:[A-Za-z0-9\-\_]+>$/ := cn) ? base : cn; +} + +str removeNewlines(str x) = visit(x) { + case /\n/ => " " +}; + + + diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/ADtoMD.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/ADtoMD.rsc new file mode 100644 index 00000000000..07aa1e8ed8f --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/ADtoMD.rsc @@ -0,0 +1,113 @@ +@synopsis{Temporary utility conversions for evolving the tutor syntax from AsciiDoc to Docusaurus Markdown} +module lang::rascal::tutor::conversions::ADtoMD + +import util::FileSystem; +import IO; +import String; + +void ad2md(loc root) { + for (f <- find(root, isSourceFile)) + convertFile(f); +} + +bool isSourceFile(loc f) = f.extension in {"md", "rsc"}; + +void convertFile(loc file) { + println("converting: <file>"); + result=for (l <- readFileLines(file)) { + append convertLine(l); + } + + writeFileLines(file, result); +} + +// [map functions]((Library:Map)) +str convertLine(/<prefix:.*>link:\/<course:[A-Za-z0-9]+>#<concept:[A-Za-z0-9\-]+>\[<title:[^\]]*>\]<postfix:.*$>/) + = convertLine("<prefix>[<title>]((<trim(course)>:<trim(concept)>))<postfix>"); + +// [Why Rascal]((WhyRascal)) +str convertLine(/<prefix:.*>link:\/<course:[A-Za-z0-9]+>\[<title:[^\]]*>\]<postfix:.*$>/) + = convertLine("<prefix>[<title>]((<trim(course)>))<postfix>"); + +// ((Hello)) +str convertLine(/<prefix:.*>\<\<<concept:[A-Za-z0-9\-]+>\>\><postfix:.*$>/) + = convertLine("<prefix>((<trim(concept)>))<postfix>"); + +// [[Extraction-Workflow]] +// ![Extraction Workflow]((define-extraction.png)) +// statement-parts.png[width="500px" style="float: right;" ,alt="Statement Types"] +str convertLine(/^<prefix:.*>image:[:]*<filename:[A-Za-z\-0-9]+>\.<ext:png|jpeg|jpg|svg>\[<properties:[^\]]*>\]<postfix:.*$>/) + = convertLine("<prefix>![<extractTitle(properties)>]((<filename>.<ext>))<postfix>"); + +// ((String-GreaterThan)) +str convertLine(/^<prefix:.*>\<\<<concept:[A-Za-z\-0-9\ ]+>,<title:[A-Za-z\-0-9\ ]+>\>\><postfix:.*$>/) + = convertLine("<prefix>((<concept>))<postfix>"); + +// ((Pattern Matching)) +str convertLine(/^<prefix:.*>\<\<<concept:[A-Za-z\-0-9\ ]+>\>\><postfix:.*$>/) + = convertLine("<prefix>((<concept>))<postfix>"); + +str convertLine(/^<prefix:.*>loctoc::\[[0-9]+\]<postfix:.*$>/) + = convertLine("<prefix>(((TOC)))<postfix>"); + +str convertLine(/^<prefix:.*>kbd:\[<keys:.*?>\]<postfix:.*$>/) + = convertLine("<prefix>`<keys>`<postfix>"); + +str convertLine(/^\ \ \ \ \*\*<postfix:.*$>/) + = convertLine(" *<postfix>"); + +// ((Library:Libraries +str convertLine(/^<prefix:.*>\(\(Libraries:<postfix:.*$>/) + = convertLine("<prefix>((Library:<postfix>"); + +str convertLine(/^<prefix:.*>\(\(Library:Libraries-<postfix:.*$>/) + = convertLine("<prefix>((Library:<postfix>"); + +// Library:Prelude- +str convertLine(/^<prefix:.*>\(\(Library:Prelude-<postfix:.*$>/) + = convertLine("<prefix>((Library:<postfix>"); + +str convertLine(/^<prefix:.*>\(\(<pre:[^\)]+>-Prelude-<lst:[^\)\-]+>\)\)<postfix:.*>$/) + = convertLine("<prefix>((<pre>-<lst>))<postfix>"); + +str convertLine(/^<prefix:.*>\(\(<pre:[^\)]+>-<fst:[^\)\-]+>-<lst:[^\)\-]+>\)\)<postfix:.*>$/) + = convertLine("<prefix>((<pre>-<fst>))<postfix>") when lst == fst, fst != "Prelude"; + +// Rascal:Concepts- +str convertLine(/^<prefix:.*>\(\(Rascal:Concepts-<rest:[^)]+>\)\)<postfix:.*>$/) + = convertLine("<prefix>((RascalConcepts:<rest>))<postfix>"); + +// italics within backquotes is not supported anymore. removing underscores for readability's sake +str convertLine(/^<prefix:.*>`<prequote:[^`]*>_<italics:[A-Za-z0-9~]+>_<postquote:[^`]*>`<postfix:.*$>/) + = convertLine("<prefix>`<prequote><italics><postquote>`<postfix>"); + +// http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-7.html#%_sec_4.1.3[procedure call] +str convertLine(/^<prefix:.*>http\:\/\/<url:[^\[\(\)]+>\[<label:[^\]\(\)]+>\]<postfix:.*$>/) + = convertLine("<prefix>[<label>](http://<url>)<postfix>"); + +str convertLine(/^keywords: \"<stuff:.*>\"\s*$/) + = "keywords: + '<for (k <- split(",", stuff)) {> - <k> + '<}>"; + +str convertLine(/^details: <stuff:.*>\s*$/) + = "details: + '<for (k <- split(",", stuff)) {> - <k> + '<}>"; + +str convertLine(/^title: \"<stuff:.*>\"\s*$/) + = "title: <stuff>"; + +str convertLine(/^\ \ \-\ \"\"<thing:[^A-Za-z0-9\-\_\ \t\"]+>\"\"\s*$/) + = " - \"<thing>\""; + +str convertLine(/^\ \ \-\ true\s*$/) + = " - \"true\""; + +str convertLine(/^\ \ \-\ false\s*$/) + = " - \"false\""; + +default str convertLine(str line) = line; + +str extractTitle(/title=\"<t:[^\"]+>\"/) = t; +default str extractTitle(str x) = ""; diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/ConvertSections.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/ConvertSections.rsc new file mode 100644 index 00000000000..d90db59263b --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/ConvertSections.rsc @@ -0,0 +1,131 @@ +module lang::rascal::tutor::conversions::ConvertSections + +import IO; +import String; +import List; +import util::FileSystem; + +void convertAllSections(loc dir) { + set[loc] files = find(dir, isConceptFile); + + for (loc f <- files) { + writeFile(f, "<for (l <- convertSections(f)) {><l> + '<}>"); + } +} + +bool isConceptFile(loc f) = (f.extension) in {"md", "concept", "rsc"}; + +bool isImageFile(loc f) = f.extension in {"png", "jpg", "svg", "jpeg"}; + +list[str] convertSections(loc file) { + return convertSections(readFileLines(file)); +} + +list[str] convertSections([str first:/^\s*\[source,rascal<rest1:.*>]\s*$/, /---/, *str block, /----*<postfix:[^\-]*>/, *str rest2]) + = [ + "```rascal<removeQuotesThing(rest1)>", + *block, + "```<postfix>", + *convertSections(rest2) + ]; + +/* +``` +image::_File_[] +image::_File_[_AlternateName_, _Width_, _Height_, link=_URI_] +``` +*/ +list[str] convertSections([str first:/^\s*\[source[^\]]*\]\s*$/, /---/, *str block, /----*<postfix:[^\-]*>/, *str rest2]) + = [ + "```", + *block, + "```<postfix>", + *convertSections(rest2) + ]; + +list[str] convertSections([str first:/^#\s+<title:.*>$/, *str rest2, /^\s*\.Index/, *str indexLines, str nextHeader:/^\s*\.[A-Z][a-z]*/, *str rest3]) + = [ + "---", + "title: \"<title>\"", + "keywords: \"<intercalate(",", words(indexLines))>\"", + "---", + *convertSections(rest2), + nextHeader, + *rest3 + ]; + +list[str] convertSections([str first:/^#\s+<title:.*>$/, *str rest2, str nextHeader:/^\s*\.[A-Z][a-z]*/, *str rest3]) + = [ + "---", + "title: <title>", + "---", + *convertSections(rest2), + nextHeader, + *rest3 + ]; + +list[str] words(list[str] input) = [ *words(line) | line <- input]; +list[str] words(str input) = [w | /<w:\S+>/ := input]; + +list[str] convertSections(["---", *str headers, "---", *str otherStuff, /^\s*\.Details/, *str detailsLines, str nextHeader:/^\s*\.[A-Z][a-z]*/, *str moreStuff]) + = [ + "---", + *headers, + *(words(detailsLines) != [] ? ["details: <intercalate(",", words(detailsLines))>"] :[]), + "---", + *otherStuff, + nextHeader, + *moreStuff + ]; + +list[str] convertSections([/^\.<headerTitle:[A-Z][A-Za-z]+>\s*$/, *str otherStuff]) + = [ + "#### <headerTitle>", + *(([str firstLine, *_] := otherStuff && trim(firstLine) != "") ? [""] : []), + *convertSections(otherStuff) + ]; + +/* + +| | | +| --- | --- | +| *What* | The pocket calculator language Calc; we already covered it ((A simple pocket calculator language)) | +| *Illustrates* | fact, define, use, requireEqual, calculate, getType, report | +| *Source* | https://github.com/cwi-swat/typepal/tree/master/src/examples/calc | + +*/ +list[str] convertSections([ + str before, + /^\s*\|====*\s*$/, + str firstLine, + *str body, + /^\s*\|====*\s*$/, + *str rest + ]) + = [ /^\s*\[[^\]*]*\]\s*$/ := before ? "" : before, + // emptyHeader(firstLine), + completeBodyLine(firstLine), + columnsLine(emptyHeader(firstLine)), + *[completeBodyLine(b) | b <- body, trim(b) != ""], + "", + *convertSections(rest) + ] when [*_, /\|====*/, *_] !:= body; + +str emptyHeader(str firstLine) = visit(completeBodyLine(firstLine)) { case /[^\|]/ => " "}; + +str completeBodyLine(str body) = /\|\s*$/ := body ? body : "<body> |"; + +str columnsLine(/\|\s*\|<postfix:.*>$/) = "| --- <columnsLine("|<postfix>")>"; +str columnsLine("|") = "|"; + +list[str] convertSections([]) + = []; + +default list[str] convertSections([str first, *str last]) + = [first, *convertSections(last)]; + +str removeQuotesThing(/^<prefix:.*>,subs=\"quotes\"<postfix:.*>/) + = "<prefix><postfix>"; + +default str removeQuotesThing(str x) = x; diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/Includes.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/Includes.rsc new file mode 100644 index 00000000000..1c0285c700d --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/Includes.rsc @@ -0,0 +1,68 @@ +module lang::rascal::tutor::conversions::Includes + +import IO; +import List; +import util::FileSystem; +import String; + +list[loc] roots = [|project://rascal/src/org/rascalmpl/library|, |project://rascal/src/org/rascalmpl/courses|]; + +list[loc] findModuleIncludes() { + for (r <- roots, f <- find(r, "md"), l:/include::\{LibDir\}<path:[^\]]+>\[tags=module\]/ <- readFileLines(f)) { + println("found: <path>"); + + theInc = |project://rascal/src/org/rascalmpl/library| + path; + + + if (!exists(theInc)) { + println("WARNING: In <f> this include does not exist: <theInc>"); + continue; + } + + theIncLines = readFileLines(theInc); + + if ([*str prelude, + /^\s*\/\/\s+tag::module\[\]\s*$/, + *str moduleContent, + /^\s*\/\/\s+end::module\[\]\s*$/, + *str rest + ] := theIncLines) { + println("<f> - <theInc>"); + } + else { + println("WARNING: <theInc> did not match any tags"); + } + } + + return []; +} + +void fixModuleIncludes() { + for (r <- roots, f <- find(r, "md")) { + fixModuleIncludes(f); + } +} + +void fixModuleIncludes(loc f) { + lines = readFileLines(f); + + writeFile(f, intercalate("\n", fixIncludes(readFileLines(f) + [""]))); +} + +// l:/include::\{LibDir\}<path:[^\]]+>\[tags=module\]/ <- readFileLines(f)) { + +list[str] fixIncludes([/include::\{LibDir\}<path:[^\]]+>\[\]/, *str tail]) + = [ *["((<lo>))" | lo := |project://rascal/src/org/rascalmpl/library/| + path], + *fixIncludes(tail) + ]; + +list[str] fixIncludes(["```rascal", l:/include::\{LibDir\}<path:[^\]]+>\.rsc\[tags=module\]/, "```", *str tail]) + = ["```rascal-include", + replaceAll(path, "/", "::"), + "```", + *fixIncludes(tail) + ]; + +default list[str] fixIncludes([str head, *str tail]) = [head, *fixIncludes(tail)]; +list[str] fixIncludes([]) = []; + diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/SplitDocTag.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/SplitDocTag.rsc new file mode 100644 index 00000000000..0fd189694cd --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/SplitDocTag.rsc @@ -0,0 +1,116 @@ +module lang::rascal::tutor::conversions::SplitDocTag + +import util::FileSystem; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; +import String; +import analysis::diff::edits::TextEdits; +import util::IDEServices; + +list[loc] findDocTags(loc root) + = [*findDocTags(parse(#start[Module], f).top) | loc f <- find(root, "rsc") ]; + +list[loc] findDocTags(Module m) + = [ t.src | /t:(Tag) `@doc <TagString _>` := m]; + + +void editLibrary(loc root) { + for (loc f <- find(root, "rsc")) { + editModule(parse(#start[Module], f).top); + } +} + +void editModule(loc example) = editModule(parse(#start[Module], example).top); +list[TextEdit] editsForModule(loc example) = rewriteDocTags(example); + +void editModule(Module m) { + edits = rewriteDocTags(m@\loc.top); + executeDocumentEdits([changed(m@\loc.top, edits)]); + return; +} + +list[TextEdit] rewriteDocTags(loc root) + = [replace(l, rewriteDocTag(l)) | l <- findDocTags(root)]; + +void doSomething() { + executeDocumentEdits([changed(|project://rascal/src/org/rascalmpl/library/Boolean.rsc|, [replace( + |project://rascal/src/org/rascalmpl/library/Boolean.rsc|(1618,210,<108,0>,<125,1>), + "\n@synopsis{\n\nConvert Boolean value to string.\n\n}\n@description{\n\nMaps `true` to `\"true\"` and `false` to `\"false\"`.\n\n}\n@examples{\n\n```rascal-shell\nimport Boolean;\ntoString(true);\ntoString(false);\n```\n}")])]); +} + +str rewriteDocTag(loc t) { + T = parse(#Tag, trim(readFile(t))); + + // drop the { } and the leading and trailing whitespace + cleanContent = trim("<T.contents>"[1..-1]); + newContent = ""; + + if (/####/ !:= cleanContent) { + // this is a synopsis + return "@synopsis{<cleanContent>}"; + } + + output = for (line <- split("\n", cleanContent)) { + println("line: <line>"); + switch (line) { + case /^####\s+[Ss]ynopsis\s*$/: { + println("SYN!"); + append "@synopsis{"; + } + case /^####\s+<heading:[A-Z][a-z]+>\s*$/: { + println("DESC! <heading>"); + append "}"; + append "@<toLowerCase(heading)>{"; + } + default: + { + // println("normal: <line>"); + append line; + } + } + } + + return " + '<for (l <- output) {><l> + '<}>}"; +} + +void executeDocumentEdits(list[DocumentEdit] edits) { + for (e <- edits) { + executeDocumentEdit(e); + } +} + +void executeDocumentEdit(removed(loc f)) { + remove(f.top); +} + +void executeDocumentEdit(created(loc f)) { + writeFile(f, ""); +} + +void executeDocumentEdit(renamed(loc from, loc to)) { + move(from.top, to.top, overwrite=true); +} + +void executeDocumentEdit(changed(loc file, list[TextEdit] edits)) { + assert isSorted(edits, less=bool (TextEdit e1, TextEdit e2) { + return e1.range.offset < e2.range.offset; + }); + + str content = readFile(file); + + for (replace(loc range, str repl) <- reverse(edits)) { + assert range.top == file.top; + content = "<content[..range.offset]><repl><content[range.offset+range.length..]>"; + } + + writeFile(file.top, content); +} + +public str example = + "#### Synopsis + ' + 'Syntax definition for S-Expressions, based on http://people.csail.mit.edu/rivest/Sexp.txt"; + diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/TrimDoc.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/TrimDoc.rsc new file mode 100644 index 00000000000..e7b10364ad2 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/conversions/TrimDoc.rsc @@ -0,0 +1,44 @@ +module lang::rascal::tutor::conversions::TrimDoc + +import util::FileSystem; +import lang::rascal::\syntax::Rascal; +import ParseTree; +import IO; +import String; + +void editLibrary(loc root) { + for (loc f <- find(root, "rsc")) { + editModule(parse(#start[Module], f)); + } +} + +void editModule(loc example) = editModule(parse(#start[Module], example).top); + +void editModule(start[Module] m) { + n = rewriteDocTags(m); + writeFile(m@\loc.top, "<n>"); + return; +} + +start[Module] rewriteDocTags(start[Module] m) = visit(m) { + case (Tag) `@synopsis <TagString c>` + => [Tag] "@synopsis{<trim("<"<c>"[1..-1]>")>}" + case jc:(Tag) `@javaClass<TagString c>` => jc + case jc:(Tag) `@contributor<TagString c>` => jc + case jc:(Tag) `@license<TagString c>` => jc + case jc:(Tag) `@expected<TagString c>` + => [Tag] "@expected{<trim("<"<c>"[1..-1]>")>}" + case jc:(Tag) `@ignoreCompiler<TagString c>` + => [Tag] "@ignoreCompiler{<trim("<"<c>"[1..-1]>")>}" + case jc:(Tag) `@ignore<TagString c>` + => [Tag] "@ignore{<trim("<"<c>"[1..-1]>")>}" + case jc:(Tag) `@tries<TagString c>` + => [Tag] "@tries{<trim("<"<c>"[1..-1]>")>}" + case jc:(Tag) `@ignoreInterpreter<TagString c>` + => [Tag] "@ignoreInterpreter{<trim("<"<c>"[1..-1]>")>}" + case (Tag) `@<Name n> <TagString c>` + => [Tag] "@<n>{ + '<trim("<"<c>"[1..-1]>")> + '}" +}; + \ No newline at end of file diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/CallAnalysis/CallAnalysis.md b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/CallAnalysis/CallAnalysis.md new file mode 100644 index 00000000000..8d42a16e188 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/CallAnalysis/CallAnalysis.md @@ -0,0 +1,119 @@ +# Synopsis +Analyzing the call structure of an application. + +# Description + +Suppose a mystery box ends up on your desk. When you open it, it contains a huge software system with several questions attached to it: + +* How many procedure calls occur in this system? +* How many procedures does it contains? +* What are the entry points for this system, i.e., procedures that call others but are not called themselves? +* What are the leaves of this application, i.e., procedures that are called but do not make any calls themselves? +* Which procedures call each other indirectly? +* Which procedures are called directly or indirectly from each entry point? +* Which procedures are called from all entry points? + +Let's see how these questions can be answered using Rascal. + +Examples: +Consider the following call graph (a box represents a procedure and an arrow represents a call from one procedure to another procedure): + +![calls]((rascal\-tutor:CallAnalysis-calls.png)) + + + +Rascal supports basic data types like integers and strings which are sufficient to formulate and answer the questions at hand. However, we +can gain readability by introducing separately named types for the items we are describing. +First, we introduce therefore a new type `Proc` (an alias for strings) to denote procedures: +```rascal-shell +alias Proc = str; +``` +Next, we have to represent the call relation as a Rascal datatype, and the relation is the most appropriate for it. +As preparation, we also import the libraries [$Rascal:Prelude/Set], [$Rascal:Prelude/Relation] and [$Rascal:Prelude/Graph] that will come in handy. +```rascal-shell-continue +import Set; +import Relation; +import analysis::graphs::Graph; +rel[Proc, Proc] Calls = {<"a", "b">, <"b", "c">, <"b", "d">, <"d", "c">, <"d", "e">, <"f", "e">, <"f", "g">, <"g", "e">}; +``` +Now we are in a good position to start asking some questions. + +__How many calls occur in this system?__ +We use the function [Rascal:Set/size] to determine the number of elements in a set or relation. +Since each tuple in the `Calls` relation represents a call between procedures, the number of tuples is equal +to the number of calls. +```rascal-shell-continue +size(Calls); +``` +__How many procedures occur in this system?__ This question is more subtle, since a procedure may call (or be called) by +several others and the number of tuples is therefore not indicative. What we need are the set of procedures that +occur (as first or second element) in _any_ tuple. This is precisely what the function [$Rascal:carrier] gives us: +```rascal-shell-continue +carrier(Calls) +``` +and computing the number of procedures is now easy: +```rascal-shell-continue +size(carrier(Calls)); +``` +As an aside, functions [$Rascal:Prelude/Relation/domain] and [$Rascal:Prelude/Relation/range] do the same for the first, respectively, second element of the pairs in a relation: +```rascal-shell-continue +domain(Calls); +range(Calls); +``` +__What are the entry points for this system?__ +The next step in the analysis is to determine which entry points this application has, i.e., procedures which call others but are +not called themselves. Entry points are useful since they define the external interface of a system and may also be used as guidance to +split a system in parts. The top of a relation contains those left-hand sides of tuples in a relation that do not occur in any +right-hand side. When a relation is viewed as a graph, its top corresponds to the root nodes of that graph. Similarly, the bottom of a +relation corresponds to the leaf nodes of the graph. See the section called ?Graph? for more details. Using this knowledge, the entry +points can be computed by determining the top of the Calls relation: +```rascal-shell-continue +top(Calls); +``` +__What are the leaves of this application?__ + +In a similar spirit, we can determine the leaves of this application, i.e., procedures that are being called but do not make any calls +themselves: +```rascal-shell-continue +bottom(Calls); +``` +__Which procedures call each other indirectly?__ + +We can also determine the indirect calls between procedures, by taking the transitive closure of the Calls relation, written as `Calls+`. +Observe that the transitive closure will contain both the direct and the indirect calls. +```rascal-shell-continue +rel[Proc,Proc] closureCalls = Calls+; +``` +__Which procedures are called directly or indirectly from each entry point?__ + +We now know the entry points for this application ("a" and "f") and the indirect call relations. Combining this information, +we can determine which procedures are called from each entry point. This is done by indexing closureCalls with appropriate procedure name. +The index operator yields all right-hand sides of tuples that have a given value as left-hand side. This gives the following: +```rascal-shell-continue +set[Proc] calledFromA = closureCalls["a"]; +set[Proc] calledFromF = closureCalls["f"]; +``` +__Which procedures are called from all entry points?__ +Finally, we can determine which procedures are called from both entry points by taking the intersection of the two sets + `calledFromA` and `calledFromF`: +```rascal-shell-continue +calledFromA & calledFromF +``` +or if your prefer to write all of the above as a one-liner using a [$Rascal:Expressions/Reducer] expression: +```rascal-shell-continue +(carrier(Calls) | it & (Calls+)[p] | p <- top(Calls)); +``` +The reducer is initialized with all procedures (`carrier(Calls)`) and iterates over all entry points (`p <- top(Calls)`). +At each iteration the current value of the reducer (`it`) is intersected (`&`) with the procedures called directly or indirectly +from that entry point (`(Calls+)[p]`). + +# Benefits +* In small examples, the above results can be easily obtained by a visual inspection of the call graph. +Such a visual inspection does _not_ scale very well to large graphs and this makes the above form of analysis particularly suited for studying large systems. + +# Pitfalls +* We discuss call analysis in a, intentionally, simplistic fashion that does not take into account how the call relation + is extracted from actual source code. + The above principles are, however, applicable to real cases as well. + + # Questions diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/CallAnalysis/calls.png b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/CallAnalysis/calls.png new file mode 100644 index 00000000000..d651f3ef3b1 Binary files /dev/null and b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/CallAnalysis/calls.png differ diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/If/If.md b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/If/If.md new file mode 100644 index 00000000000..d33ffc29803 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/If/If.md @@ -0,0 +1,43 @@ +## If + +#### Synopsis + +Conditional statement. + +#### Syntax + +* `if ( Exp ) Statement;` +* `if ( Exp ) Statement~1~ else Statement~2~;` + +#### Types + +| `Exp` | `if ( Exp ) Statement;` | +| --- | --- | +| `bool` | `void` | + + +| `Exp` | Statement~1~ | Statement~2~ | `if ( Exp ) Statement~1~ else Statement~2~;` | +| --- | --- | --- | --- | +| `bool` | T~1~ | T~2~ | `lub(T~1~, T~2~)` | + + + +#### Description + +The test _Exp_ is evaluated and its outcome determines the statement to be executed: +_Statement~1~_ if _Exp_ yields `true` and _Statement~2~_ otherwise. +The value of an if-then statement is equal to _Statement_ when its test is true. Otherwise it is void. +The value of an if-then-else statement is the value of the statement that was executed. + +#### Examples + +```rascal-shell +if( 3 > 2 ) 30; else 40; +x = if( 3 > 2 ) 30; else 40; +if( 3 > 2 ) 30; +``` +An if-then statement yields `void` when its test is false +(demonstrated by the __ok__ that is printed by the Rascal system): +```rascal-shell-continue +if( 2 > 3 ) 30; +``` diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Libraries/Boolean/Boolean.remote b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Libraries/Boolean/Boolean.remote new file mode 100644 index 00000000000..8816436b0ab --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Libraries/Boolean/Boolean.remote @@ -0,0 +1 @@ +|std:///Boolean.rsc| \ No newline at end of file diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Libraries/Libraries.md b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Libraries/Libraries.md new file mode 100644 index 00000000000..27e5bad4c8f --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Libraries/Libraries.md @@ -0,0 +1,2 @@ +## Synopsis +Library functions diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Questions/Questions.questions b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Questions/Questions.questions new file mode 100644 index 00000000000..175e89bc169 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Questions/Questions.questions @@ -0,0 +1,66 @@ +question Which means of transportation is faster: + choice n ||| Apache Helicopter ||| The speed of an Apache is 293 km/hour + choice y ||| High-speed train ||| The speed of a high-speed train is 570 km/hour + choice n ||| Ferrari F430 ||| The speed of a Ferrari is 315 km/hour + choice n ||| Hovercraft ||| The speed of a Hovercraft is 137 km/hour +end + +question Replace the text box by the result of the multiplication and make the test true: + expr multiplication $gen(int[2,7],A) * $gen(int[2,7],B) +end + +question Replace the text box by the result of the intersection and make the test true: + expr setIntersection $eval($gen(set[int]) + $gen(set[int],B)) & $eval($gen(set[int]) + $use(B)) +end + +question Replace the text box by a function name and make the test true: + prep import List; + expr listFunction $answer(size)($gen(list[int][1,10])) +end + +question Create a function to print squares by placing all code fragments in the grey box in the right order with the right indentation: +movable +void squares(int n){ +--------- + println("Squares from 1 to " + n); +--------- + for(int i <- [1 .. n + 1]) +--------- + println(i + " squared = " + (i * i)); +--------- +} +end + +question Create a function to print squares by placing all code fragments in the grey box in the right order with the right indentation (and avoid decoys!): +movable +void squares(int n){ +--------- + println("Squares from 1 to " + n); +--------- + for(int i <- [1 .. n + 1]) +--------- + println(i + " squared = " + (i * i)); +--------- +} +decoy +i = 0; +--------- +i += 1; +--------- +println(i + " squared = " + (i + i)); +end + +question Click on all identifiers in this code fragment: +clickable +$click(x) = 1; +if($click(x)){ + $click(y) = $click(x) + 2; +} +end + +question Reorder the following items and make all statements true: + fact [1,2,3] ||| is a list[int] + fact {1,2,3} ||| is a set[int] + fact 123 ||| is an int + fact "abc" ||| is a str +end \ No newline at end of file diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Test.md b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Test.md new file mode 100644 index 00000000000..68a03c04c26 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Test.md @@ -0,0 +1,50 @@ +# Synopsis +This is a test synopsis. + +# Description +See examples below!!! nieuwe content + + + +# Examples + +```rascal-shell +import Content; +html("this is some \<strong\>HTML\</strong\> output") +file(|https://www.rascal-mpl.org/assets/ico/favicon.png|) +1 + 1 == 2 +int count = 1; +content("counter", Response (Request _) { count += 1; return response("count: <count>"); }) +count; +count = 66; +content("counter", Response (Request _) { count += 1; return response("count: <count>"); }) +count; +``` + +* _emphasis_ +* *bold* +* [Rascal Web site](http:///rascal-mpl.org) +* ((CallAnalysis)) +* Table: + + | Module | LOC | + |--------|-----| + | A | 10 | + | B | 20 | + + +| Operator | Description | +|------------|------------| +| `$A$ \| $B$` | alternative | +| `\|\|` | or | + +Horizontal rule: + +--- + +* `code` +* `in code: italics` + +# Benefits + +# Pitfalls diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Test.quest b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Test.quest new file mode 100644 index 00000000000..96128c7bd7b --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Test.quest @@ -0,0 +1 @@ +[tvQuestion("Test","1",valueOfExpr(),details(" Fill in the missing function name!\n",["import List;"],"","","","(\<L\>) == \<H\>",false,true,[<"L",list(arb(0,[int(-20,20),str()]),0,5)>],[<"H","reverse(\<L\>) ">],void(),"reverse"))] \ No newline at end of file diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Vis/Example.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Vis/Example.rsc new file mode 100644 index 00000000000..cdd4c2bcf9b --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Vis/Example.rsc @@ -0,0 +1,15 @@ +@synopsis{Just a module} +module Vis::Example + +syntax VIS = "vis"; + +@synopsis{main is marvelous} +@description{ +```rascal-shell +import IO; +println("hai") +``` +} +int main() { + return 0; +} \ No newline at end of file diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Vis/Vis.md b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Vis/Vis.md new file mode 100644 index 00000000000..39244ffd3da --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/Vis/Vis.md @@ -0,0 +1,39 @@ +--- +title: Visuals Test +--- + +```rascal-shell +import lang::html::IO; +serve(p([text("This is"), b([text("bold")]), text("!")])) +``` + +```rascal-commands,continue + +import IO; + +void exampleFunction() { + + println("HelloWorld!"); + +} +``` + +```rascal-shell,continue +exampleFunction(); +``` + +```rascal-shell +import lang::html::IO; +HTMLElement table(rel[&T, &U] r) + = table([ + tr([ + td([text("<a>")]), + td([text("<b>")]) + ]) + | <a, b> <- r + ]); +characters = {"Sneezy", "Sleepy", "Dopey", "Doc", "Happy", "Bashful", "Grumpy"}; +serve(table(characters * characters)) +``` + + diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/t1.png b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/t1.png new file mode 100644 index 00000000000..7a113ac8fa2 Binary files /dev/null and b/src/org/rascalmpl/tutor/lang/rascal/tutor/examples/Test/t1.png differ diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/ConvertQuestions.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/ConvertQuestions.rsc new file mode 100644 index 00000000000..284847a7fba --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/ConvertQuestions.rsc @@ -0,0 +1,71 @@ +module lang::rascal::tutor::questions::ConvertQuestions + +import IO; +import List; +import String; +import util::FileSystem; +/* + +Name: => # the_name +the_name + +Synopsis: => .Synopsis +enz. + +<listing> => ```rascal PM from file +</listing> => ``` + +<screen> => ```rascal-shell PM errors en continue +</screen> => ``` + +$VarName$ => _VarName_ +$VarName_index$ => _VarName~index~_ +$VarName^index$ => _VarName^index^_ + +<warning> => WARNING: +</warning> => "" + +[ConceptName] => +[$ConceptName] => +$OtherCourse:ConceptName] => + + +In code: + +*/ + +str getAnchor(loc src){ + parts = split("/", src.path); + return "<parts[-3]>-<parts[-2]>"; +} + +str convert(loc src){ + return convert(getAnchor(src), readFileLines(src)); +} + +str hashes(int n) = "<for(int _ <-[0..n]){>#<}>"; + +str convert(str _/*anchor; why is this not used?*/, list[str] lines){ + result = ""; //"[[<anchor>]]\n"; + int i = 0; + + while (i < size(lines)){ + switch(lines[i]){ + + + case /^Questions:/: + { i += 1; + while(i < size(lines)){ + result += lines[i] + "\n"; + i += 1; + } + return result; + } + // anything else + default: { + i += 1; + } + } + } + return result; +} diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/Feedback.java b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/Feedback.java new file mode 100644 index 00000000000..9e51db1e089 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/Feedback.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2016, paulklint, Centrum Wiskunde & Informatica (CWI) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.tutor.lang.rascal.tutor.questions; + +import java.util.Random; + +public class Feedback { + + static Random random = new Random(); + + private static String[] positiveFeedback = { + "Good!", + "Go on like this!", + "I knew you could make this one!", + "You are making good progress!", + "Well done!", + "Yes!", + "More kudos", + "Correct!", + "You are becoming a pro!", + "You are becoming an expert!", + "You are becoming a specialist!", + "Excellent!", + "Better and better!", + "Another one down!", + "You are earning a place in the top ten!", + "Learning is fun, right?", + "Each drop of rain makes a hole in the stone.", + "A first step of a great journey.", + "It is the journey that counts.", + "The whole moon and the entire sky are reflected in one dewdrop on the grass.", + "There is no beginning to practice nor end to enlightenment; There is no beginning to enlightenment nor end to practice.", + "A journey of a thousand miles begins with a single step.", + "When you get to the top of the mountain, keep climbing.", + "No snowflake ever falls in the wrong place.", + "Sitting quietly, doing nothing, spring comes, and the grass grows by itself.", + "To follow the path, look to the master, follow the master, walk with the master, see through the master, become the master.", + "When you try to stay on the surface of the water, you sink; but when you try to sink, you float.", + "If you realize that you have enough, you are truly rich.", + "Experience this moment to its fullest.", + "Many paths lead from the foot of the mountain, but at the peak we all gaze at the full moon.", + "As a solid rock is not shaken by the wind, even so the wise are not ruffled by praise or blame.", + "When you get there, there isn't any there there.", + "At the still-point in the center of the circle one can see the infinite in all things.", + "The whole moon and the entire sky are reflected in one dewdrop on the grass.", + "When the question is sand in a bowl of boiled rice, the answer is a stick in the soft mud." + }; + + private static String[] negativeFeedback = { + "A pity!", + "A shame!", + "Try another question!", + "I know you can do better.", + "Keep trying.", + "I am suffering with you :-(", + "Give it another try!", + "With some more practice you will do better!", + "Other people mastered this, and you can do even better!", + "It is the journey that counts!", + "Learning is fun, right?", + "After climbing the hill, the view will be excellent.", + "Hard work will be rewarded!", + "There\'s no meaning to a flower unless it blooms.", + "Not the wind, not the flag; mind is moving.", + "If you understand, things are just as they are; if you do not understand, things are just as they are.", + "Knock on the sky and listen to the sound.", + "The ten thousand questions are one question. If you cut through the one question, then the ten thousand questions disappear.", + "To do a certain kind of thing, you have to be a certain kind of person.", + "When the pupil is ready to learn, a teacher will appear.", + "If the problem has a solution, worrying is pointless, in the end the problem will be solved. If the problem has no solution, there is no reason to worry, because it can\'t be solved.", + "And the end of all our exploring will be to arrive where we started and know the place for the first time.", + "It is better to practice a little than talk a lot.", + "Water which is too pure has no fish.", + "All of the significant battles are waged within the self.", + "No snowflake ever falls in the wrong place.", + "It takes a wise man to learn from his mistakes, but an even wiser man to learn from others.", + "Only when you can be extremely pliable and soft can you be extremely hard and strong.", + "Sitting quietly, doing nothing, spring comes, and the grass grows by itself.", + "The obstacle is the path.", + "To know and not do is not yet to know.", + "The tighter you squeeze, the less you have.", + "When you try to stay on the surface of the water, you sink; but when you try to sink, you float.", + "Where there is will, there is a way.", + "The grass is always greener on the other side.", + "Change how you see and see how you change.", + "If you understand, things are just as they are. If you do not understand, things are just as they are.", + "The wild geese do not intend to cast their reflections.The water has no mind to receive their images.", + "No matter how hard the past, you can always begin again.", + "You can't stop the waves, but you can learn how to surf.", + "Scratch first. Itch later ... ", + "It takes a wise man to learn from his mistakes, but an even wiser man to learn from others.", + "Nature does not hurry, yet everything is accomplished.", + "One gains by losing and loses by gaining.", + "Relearn everything. Let every moment be a new beginning.", + "The world is won by those who let it go.", + "Normally, we do not so much look at things as we overlook them.", + "If you want a certain thing, first be a certain person. Then obtaining that certain thing will no longer be a concern.", + "Nothing ever goes away, until it has taught us what we need to know.", + "Don't get stuck anywhere, keep flowing like a river.", + "The birds always find their way to their nests. The river always finds its way to the ocean.", + "Do not seek the truth, only cease to cherish your opinions.", + "The resistance to the unpleasant situation is the root of suffering.", + "For things to reveal themselves to us, we need to be ready to abandon our views about them.", + "To deny the reality of things is to miss their reality", + "View all problems as challenges." + }; + + private static String quote(String s){ + return "\"" + s + "\""; + } + public static String give(boolean ok) { + if(random.nextInt(10) == 5){ + if(ok){ + return quote(positiveFeedback[random.nextInt(positiveFeedback.length)]); + } else { + return quote(negativeFeedback[random.nextInt(negativeFeedback.length)]); + } + } + return quote(""); + } +} diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/ParseQuestions.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/ParseQuestions.rsc new file mode 100644 index 00000000000..5e44d95951a --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/ParseQuestions.rsc @@ -0,0 +1,8 @@ +module lang::rascal::tutor::questions::ParseQuestions + +import lang::rascal::tutor::questions::Questions; +import ParseTree; + +public Questions parse(str src, loc l) = parse(#start[Questions], src, l).top; +public Questions parse(str src) = parse(#start[Questions], src).top; +public Questions parse(loc l) = parse(#start[Questions], l).top; diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/QuestionCompiler.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/QuestionCompiler.rsc new file mode 100644 index 00000000000..e67f7ec29c0 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/QuestionCompiler.rsc @@ -0,0 +1,530 @@ +module lang::rascal::tutor::questions::QuestionCompiler + +import IO; +import String; +import util::Math; +import List; +import Set; +import String; + +import IO; +import util::Reflective; +import util::Eval; +import DateTime; +import ParseTree; + +import lang::rascal::tutor::questions::Questions; +import lang::rascal::tutor::questions::ParseQuestions; +import lang::rascal::tutor::questions::ValueGenerator; + +int countGenAndUse((Cmd) `<EvalCmd c>`){ + n = 0; + visit(c){ case GenCmd _: n += 1; + case UseCmd _: n += 1; + } + return n; +} + +str holeMarkup(int n) = "+++\<div class=\"hole\" id=\"hole<n>\"/\>+++"; +str clickMarkup(int n, str text) = "+++\<span class=\"clickable\" id=\"clickable<n>\" clicked=\"false\" onclick=\"handleClick(\'clickable<n>\')\"\><text>\</span\>+++"; + +tuple[str quoted, str execute, bool hasHoles, map[str,str] bindings] preprocessCode(int questionId, + TokenOrCmdList q, + str setup, + map[str,str] initialEnv, + PathConfig pcfg){ + nholes = 0; + map[str,str] env = initialEnv; + + + tuple[str quote, str execute] handleCmd((Cmd) `<GenCmd gen>`){ + switch(gen){ + case (GenCmd) `$gen(<Type tp>,<Name name>)`: { + str v = generateValue(generateType(tp)); + if(env["<name>"]?){ + throw "Double declaration for <name> in <gen>"; + } + env["<name>"] = v; + return <v, v>; + } + case (GenCmd) `$gen(<Type tp>)`: { + Type tp1 = generateType(tp); + v = generateValue(generateType(tp)); + return <v, v>; + } + default: + throw "Unhandled $gen <gen>"; + } + } + + tuple[str quote, str execute] handleCmd((Cmd) `<UseCmd use>`){ + if(!env["<use.name>"]?){ + throw "Undeclared <use.name> used in <use>"; + } + v = env["<use.name>"]; + return <v, v>; + } + + tuple[str quote, str execute] handleCmd((Cmd) `<EvalCmd c>`){ + <qt, expr> = handle(c.elements); + v = "<eval(questionId, expr, setup, pcfg)>"; + return <v, v>; + } + + tuple[str quote, str execute] handleCmd((Cmd) `<AnswerCmd ans>`){ + nholes += 1; + txt = "<ans.elements>"; + qt = holeMarkup(nholes); + try { + gen = [Cmd] "$gen(<txt>)"; + <qt1, ex> = handleCmd(gen); + return <qt, ex>; + } catch: { + try { + c = [Cmd] "$eval(<txt>)"; + if(countGenAndUse(c) == 0) throw ""; + <qt1, ex1> = handleCmd(c); + return <qt, ex1>; + } catch : { + v = "<txt>"; + return <qt, v>; + } + } + } + + tuple[str quote, str execute] handle(TokenOrCmd toc){ + if(toc is aCmd){ + return handleCmd(toc.aCmd); + } else { + v = "<toc>"; + return <v, v>; + } + } + + tuple[str quote, str execute] handle(TokenOrCmdList tocList){ + if(appl(_,list[Tree] args) := tocList) { + qt = ""; + ex = ""; + if(tocList is parens){ + qt += "(<args[1]>"; ex += "(<args[1]>"; + <qt1, ex1> = handle(tocList.tocList); + qt += "<qt1><args[3]>)"; ex += "<ex1><args[3]>)"; + if(TokenOrCmdList toc2 <- tocList.optTocList){ + qt += "<args[5]>"; ex += "<args[5]>"; + <qt2, ex2> = handle(toc2); + qt += qt2; ex += ex2; + } + } else { + <qt, ex> = handle(tocList.toc); + if(TokenOrCmdList toc2 <- tocList.optTocList){ + qt += "<args[1]>"; ex += "<args[1]>"; + <qt1, ex1> = handle(toc2); + qt += qt1; ex += ex1; + } + } + return <qt, ex>; + } else { + throw "Cannot match parse tree: <tocList>"; + } + } + + <qt, ex> = handle(q); + return <qt, ex, nholes > 0, env>; +} + +str preprocessClick(int _, TokenOrCmdList q){ + nholes = 0; + + tuple[str quote, str execute] handleCmd((Cmd) `<ClickCmd cc>`){ + nholes += 1; + txt = "<cc.elements>"; + qt = clickMarkup(nholes, txt); + return <qt, qt>; + } + + tuple[str quote, str execute] handle(TokenOrCmd toc){ + if(toc is aCmd){ + return handleCmd(toc.aCmd); + } else { + v = "<toc>"; + return <v, v>; + } + } + + tuple[str quote, str execute] handle(TokenOrCmdList tocList){ + if(appl(_,list[Tree] args) := tocList) { + qt = ""; + ex = ""; + if(tocList is parens){ + qt += "(<args[1]>"; ex += "(<args[1]>"; + <qt1, ex1> = handle(tocList.tocList); + qt += "<qt1><args[3]>)"; ex += "<ex1><args[3]>)"; + if(TokenOrCmdList toc2 <- tocList.optTocList){ + qt += "<args[5]>"; ex += "<args[5]>"; + <qt2, ex2> = handle(toc2); + qt += qt2; ex += ex2; + } + } else { + <qt, ex> = handle(tocList.toc); + if(TokenOrCmdList toc2 <- tocList.optTocList){ + qt += "<args[1]>"; ex += "<args[1]>"; + <qt1, ex1> = handle(toc2); + qt += qt1; ex += ex1; + } + } + return <qt, ex>; + } else { + throw "Cannot match parse tree: <tocList>"; + } + } + + <qt, ex> = handle(q); + return qt; +} + +int questionId = 0; + +str removeComments(Intro? intro){ + res = ""; + for(line <- split("\n", "<intro>")){ + if(!startsWith(line, "//")){ + res += line + "\n"; + } + } + + return res; +} + +public str compileQuestions(loc qloc, PathConfig pcfg) { + pcfg = pathConfig(srcs=[|memory://test-modules/|]+pcfg.srcs,libs=pcfg.libs,bin=pcfg.bin); + return process(qloc, pcfg); +} + +str process(loc qloc, PathConfig pcfg){ + iqs = parse(qloc); + questionId = 0; + + bn = qloc.file; + if(/^<b:.*>\..*$/ := qloc.file) bn = b; + + res = "# <bn> + ' + '++++ + '\<script src=\"http:///code.jquery.com/jquery-3.1.1.js\"\>\</script\> + '\<script type=\"text/javascript\" src=\"https://code.jquery.com/ui/1.11.4/jquery-ui.min.js\"\>\</script\> + '++++ + '"; + for (iq <- iqs.introAndQuestions) { + intro = removeComments(iq.intro); + res += (intro + "\n" + process("<iq.description>", iq.question, pcfg) +"\n"); + } + res += " + '++++ + '\<script src=\"tutor-prelude.js\"\>\</script\> + '++++ + '"; + return res; +} + +// ---- CodeQuestion + +str process(str text, (Question) `<CodeQuestion q>`, PathConfig pcfg){ + prep_quoted = prep_executed = ""; + prep_holes = false; + env1 = (); + questionId += 1; + + if(Prep p <- q.prep){ + <prep_quoted, prep_executed, prep_holes, env1> = preprocessCode(questionId, p.text, "", (), pcfg); + } + + expr_quoted = expr_executed = ""; + expr_holes = false; + env2 = env1; + if(Expr e <- q.expr){ + <expr_quoted, expr_executed, expr_holes, env2> = preprocessCode(questionId, e.text, prep_executed, env1, pcfg); + return codeQuestionMarkup(questionId, text, + "module Question<questionId> + '<prep_quoted><"<prep_quoted>" == "" ? "" : "\n"> + 'test bool <e.name>() = + ' <expr_quoted> == <expr_holes ? eval(questionId, expr_executed, prep_executed, pcfg) : holeMarkup(1)>; + '"); + } + if(prep_holes){ + runTests(questionId, prep_executed, pcfg); + return codeQuestionMarkup(questionId, text, "module Question<questionId> + '<prep_quoted> + "); + } + throw "Incorrect question: no expr given and no holes in prep code"; +} + +str replaceHoles(str code){ + return replaceAll(visit(code){ + case /^\+\+\+[^\+]+\+\+\+/ => "_" + }, "\n", "\\n"); +} + +str escape(str code){ + return replaceAll(code, "\"", """); +} + +str removeSpacesAroundHoles(str code){ + return visit(code){ + case /^[ ]+\+\+\+/ => " +++" + case /^\+\+\+[ ]+/ => "+++ " + }; +} + +str codeQuestionMarkup(int n, str text, str code){ + return ".Question <n> + '<text> + '++++ + '\<div id=\"Question<n>\" + ' class=\"code-question\" + ' listing=\"<escape(replaceHoles(code))>\"\> + '++++ + '[source,rascal,subs=\"normal\"] + '---- + '<removeSpacesAroundHoles(code)> + '---- + '++++ + '\</div\> + '++++"; +} + +// ---- ChoiceQuestion + +str process(str text, (Question) `<ChoiceQuestion q>`, PathConfig pcfg){ + questionId += 1; + return choiceQuestionMarkup(questionId, text, q.choices); +} + +str choiceQuestionMarkup(int n, str explanation, Choice* choices){ + return ".Question <n> + '<explanation> + '++++ + '\<div id=\"Question<n>\" + ' class=\"choice-question\"\> + '<for(ch <- choices){> + ' \<input type=\"radio\" class=\"choice-input\" name=\"Question<n>\" value=\"<ch.correct>\" feedback=\"<trim("<ch.feedback>")>\"\> + ' \<div class=\"choice-description\"\> <ch.description>\</div\> + '<}> + '\</div\> + '++++ + '"; +} + +// ---- ClickQuestion + +str process(str explanation, (Question) `<ClickQuestion q>`, PathConfig pcfg){ + questionId += 1; + qt = preprocessClick(questionId, q.text); + + return clickQuestionMarkup(questionId, explanation, qt); +} + +str clickQuestionMarkup(int n, str explanation, str code){ + return ".Question <n> + '<explanation> + '++++ + '\<div id=\"Question<n>\" + ' class=\"click-question\"\> + '++++ + '[source,rascal,subs=\"normal\"] + '---- + '<code> + '---- + '++++ + '\</div\> + '++++ + '"; +} + +// ---- MoveQuestion + +str process(str explanation, (Question) `<MoveQuestion q>`, PathConfig pcfg){ + questionId += 1; + if(Decoy d <- q.decoy){ + return moveQuestionMarkup(questionId, explanation, "<q.text>", "<d.text>"); + } else { + return moveQuestionMarkup(questionId, explanation, "<q.text>", ""); + } +} + +data Fragment = fragment(int index, list[str] lines, int pre, int post); + +int indent(str s) = /^<a:[ ]*>/ := s ? size(a) : 0; + +list[list[str]] makeSegments(list[str] lines){ + list[list[str]] segments = []; + if(any(str line <- lines, startsWith(line, "---"))){ + cur = []; + for(str line <- lines){ + if(startsWith(line, "---")){ + segments += [cur]; + cur = []; + } else { + cur += [line]; + } + } + if(size(cur) > 0){ + segments += [cur]; + } + } else { + for(int i <- index(lines)){ + if(i + 2 < size(lines)){ + segments += [lines[i .. i + 2]]; + } else { + segments += [lines[i ..]]; + } + } + } + return segments; +} + +str moveQuestionMarkup(int n, str explanation, str code, str decoy){ + code_lines = split("\n", code); + segments = makeSegments(code_lines); + fragments = [fragment(i, segments[i], indent(segments[i][0]), indent(segments[i][-1])) | i <- index(segments)]; + + decoy_fragments = []; + decoy_lines = split("\n", decoy); + if(size(decoy) > 0){ + decoy_segments = makeSegments(decoy_lines); + decoy_fragments = [fragment(-1, decoy_segments[i], 0, 0) | i <- index(decoy_segments)]; + } + + gcode = ""; + ftop = -260; + initialIndent = fragments[0].pre; + + for(f <- shuffle(fragments + decoy_fragments)){ + + minIndent = min(f.pre, f.post); + flines = ""; + for(line <- f.lines){ + flines += line[minIndent..] + "\n"; + } + gcode += "\<div id=\"box-<f.index>\" class=\"movable-code\" index=\"<f.index>\" indent=\"<(f.pre - initialIndent)/2>\" style=\"position:relative;top:<ftop>px;\"\> + '\<pre\>\<code\>" + + + "<flines>\</code\>\</pre\>\</div\>\n"; + ftop += size(f.lines) * 9; + } + id = "Question<n>"; + return + ".Question <n> + '<explanation> + '++++ + '\<div id=\"<id>\" + ' class=\"move-question\"\> + '\<div id=\"movable-code-src-<id>\" class=\"movable-code-src\" \"lines=\"<size(code_lines + decoy_lines)>\"\> + '\<div id=\"movable-code-target-<id>\" class=\"movable-code-target\"\> + '\</div\> + " + + gcode + + + "\</div\> + '\</div\> + '++++ + '"; + + // '\<form id=\"movable-code-form-<id>\" class=\"movable-code-form\"\> + //'\<input type=\"submit\" value=\"Submit Answer\"\> + // '\</form\> +} + +// ---- FactQuestion +str process(str explanation, (Question) `<FactQuestion q>`, PathConfig pcfg){ + questionId += 1; + return factQuestionMarkup(questionId, explanation, q.facts); +} + +str factQuestionMarkup(int n, str explanation, Fact+ facts){ + s1 = []; + s2 = []; + + afacts = [f | f <- facts]; + + for(int i <- index(afacts)){ + fact = afacts[i]; + s1 += ["\<li index=\"<i>\" class=\"fact-item\"\>\<tt\><trim("<fact.leftText>")>\</tt\>\</li\>\n"]; + s2 += ["\<li index=\"<i>\" class=\"fact-item\"\>\<tt\><trim("<fact.rightText>")>\</tt\>\</li\>\n"]; + } + + id = "Question<n>"; + return + ".Question <n> + '<explanation> + '++++ + '\<div id=\"<id>\" class=\"fact-question\"\> + '\<ul id=\"sortable1-<id>\" class=\"sortableLeft\"\> + '<intercalate("\n", shuffle(s1))> + '\</ul\> + '\<ul id=\"sortable2-<id>\" class=\"sortableRight\"\> + '<intercalate("\n", shuffle(s2))> + '\</ul\> + '\</div\> + '++++ + '"; +} + +// ---- + + +loc makeQuestion(int questionId, PathConfig pcfg){ + for(f <- pcfg.bin.ls){ + if(/Question/ := "<f>"){ + remove(f); + } + } + mloc = |memory://test-modules/| + "Question<questionId>.rsc"; + return mloc; +} + +value eval(int questionId, str exp, str setup, PathConfig pcfg) { + Q = makeQuestion(questionId, pcfg); + msrc = "module Question<questionId> + ' + '<setup> + ' + 'value main() { + ' return <exp>; + '}"; + + writeFile(Q, msrc); + + try { + if (result(value res) := eval(#value, ["import Question<questionId>;", "main();"])) { + return res; + } + else { + throw "evaluation of <exp> failed"; + } + } catch e:{ + println("*** While evaluating <exp> in + ' <msrc> + '*** the following error occurred: + ' <e>"); + throw "Error while evaluating expression <exp>"; + } +} + +void runTests(int questionId, str mbody, PathConfig pcfg){ + Q = makeQuestion(questionId, pcfg); + msrc = "module Question<questionId> <mbody>"; + writeFile(Q, msrc); + try { + if (result(false) == eval(#bool, ["import Question<questionId>;", ":test"])) { + throw "some test failed"; + } + } catch value e: { + println("*** While running tests for + ' <msrc> + '*** the following error occurred: + ' <e>"); + throw "Error while running tests"; + } +} + diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/Questions.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/Questions.rsc new file mode 100644 index 00000000000..d3f0408338a --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/Questions.rsc @@ -0,0 +1,253 @@ +module lang::rascal::tutor::questions::Questions + +import ParseTree; + +// Syntax of the Question language + +lexical LAYOUT = [\ \t \n]; + +layout Layout = LAYOUT* !>> [\ \t \n]; + +start syntax Questions = IntroAndQuestion+ introAndQuestions; + +syntax IntroAndQuestion + = Intro? intro "question" Tokens description Question question "end"; + +syntax Question + = CodeQuestion + | ChoiceQuestion + | ClickQuestion + | MoveQuestion + | FactQuestion + ; + +syntax CodeQuestion + = Prep? prep Expr? expr; + +syntax Prep + = "prep" TokenOrCmdList text; + +syntax Expr + = "expr" Name name TokenOrCmdList text; + +syntax ChoiceQuestion + = Choice+ choices; + +syntax Choice + = "choice" YesOrNo correct "|||" Tokens description "|||" Tokens feedback + ; + +lexical YesOrNo = "y" | "n"; + +syntax ClickQuestion + = "clickable" TokenOrCmdList text; + +syntax MoveQuestion + = "movable" Tokens text Decoy? decoy; + +syntax Decoy + = "decoy" Tokens text; + +syntax FactQuestion + = Fact+ facts; + +syntax Fact + = "fact" Tokens leftText "|||" Tokens rightText; + +keyword Reserved + = "question" + | "prep" + | "expr" + | "end" + | "choice" + | "clickable" + | "movable" + | "decoy" + | "fact" + | "$answer" + | "$gen" + | "$use" + | "$eval" + | "$click" + ; + +lexical Name = [$]? [A-Za-z] [A-Z a-z 0-9 _]* !>> [A-Z a-z 0-9 _]; + +lexical IntCon = "-"?[0-9]+ !>> [0-9]; + +syntax Intro = AnyText+ !>> "question"; + +lexical AnyText = Name \ Reserved | IntCon | ![$ a-z A-Z 0-9 \ \t \n] ; + +lexical AnyButSpecial = Name \ Reserved | IntCon | ![$ ( ) \\ a-z A-Z 0-9 \t \n] ; + +lexical Escaped + = "\\" ( "(" | ")" | "$" | Reserved) + ; + +syntax Cmd + = AnswerCmd + | GenCmd + | UseCmd + | EvalCmd + | ClickCmd + ; +syntax AnswerCmd + = "$answer" "(" TokenOrCmdList elements ")" + ; +syntax GenCmd + = "$gen" "(" Type type ")" + | "$gen" "(" Type type "," Name name ")" + ; +syntax UseCmd + = "$use" "(" Name name ")" + ; +syntax EvalCmd + = "$eval" "(" TokenOrCmdList elements ")" + ; +syntax ClickCmd + = "$click" "(" Tokens elements ")" + ; + +syntax Token + = Escaped + > AnyButSpecial + ; + +syntax Tokens + = "(" Tokens ")" Tokens? + | Token Tokens? + ; + +syntax TokenOrCmd + = aCmd: Cmd aCmd + > aToken: Token aToken + ; + +syntax TokenOrCmdList + = parens: "(" TokenOrCmdList tocList ")" TokenOrCmdList? optTocList + | noparens: TokenOrCmd toc TokenOrCmdList? optTocList + ; + +keyword TypeNames + = "bool" + | "int" + | "real" + | "num" + | "str" + | "loc" + | "datetime" + | "list" + | "set" + | "map" + | "tuple" + | "rel" + | "lrel" + | "value" + | "void" + | "arb" + ; + +syntax Type + = "bool" + | "int" + | "int" Range range + | "real" + | "real" Range range + | "num" + | "num" Range range + | "str" + | "loc" + | "datetime" + | "list" "[" Type elemType "]" + | "list" "[" Type elemType "]" Range range + | "set" "[" Type elemType "]" + | "set" "[" Type elemType "]" Range range + | "map" "[" Type keyType "," Type valType "]" + | "map" "[" Type keyType "," Type valType "]" Range range + | "tuple" "[" {Type ","}+ elemTypes "]" + | "tuple" "[" {Type ","}+ elemTypes "]" Range range + | "rel" "[" {Type ","}+ elemTypes "]" + | "lrel" "[" {Type ","}+ elemTypes "]" + | "value" + | "void" + | "arb" "[" IntCon depth "," {Type ","}+ elemTypes "]" + ; + +syntax Range = "[" IntCon min "," IntCon max "]" range; + +public Questions parse(str src) = parse(#start[Questions], src).top; + +value pmain(){ +/* + +"123, 456 + 'question Replace text box by a function name and make the test true. + 'prep import List; + 'expr listFunction $answer(headTail)($gen(list[int][1,10])) + 'end + + 'question Replace text box by the result of the intersection and make the test true. + 'expr setIntersection $eval($gen(set[int]) + $gen(set[int],B)) & $eval($gen(set[int]) + $use(B)) + 'end + ' $abc ( + 'question Which means of transportation is faster + choice Apache Helicopter + correct no + feedback The speed of an Apache is 293 km/hour + choice High-speed train + correct yes + feedback The speed of high-speed train is 570 km/hour + choice Ferrari F430 + correct no + feedback The speed of a Ferrari is 315 km/hour + choice Hovercraft + correct no + feedback The speed of a Hovercraft is 137 km/hour + end + question Click on all identifiers in this code fragment: + 'clickable + $click(x) = 1; + $click(y) = $click(x) + 2; + 'end" + */ + +return parse("123, 456 + 'question Replace text box by a function name and make the test true. + 'prep import List; + 'expr listFunction $answer(headTail)($gen(list[int][1,10])) + 'end + + 'question Replace text box by the result of the intersection and make the test true. + 'expr setIntersection $eval($gen(set[int]) + $gen(set[int],B)) & $eval($gen(set[int]) + $use(B)) + 'end + ' $abc ( + 'question Which means of transportation is faster + choice Apache Helicopter + correct no + feedback The speed of an Apache is 293 km/hour + choice High-speed train + correct yes + feedback The speed of high-speed train is 570 km/hour + choice Ferrari F430 + correct no + feedback The speed of a Ferrari is 315 km/hour + choice Hovercraft + correct no + feedback The speed of a Hovercraft is 137 km/hour + end + question Click on all identifiers in this code fragment: + 'clickable + $click(x) = 1; + $click(y) = $click(x) + 2; + 'end" +/* + "question[code] Replace text box by a function name and make the test true. + ' prep import List; + ' expr listFunction: $answer[headTail]($gen[list[int][1,10]]) + 'end + 'question[code] Replace text box by the result of the intersection and make the test true. + ' expr setIntersection: $eval[ $gen[set[int]] + $gen[set[int],B] } & $eval[$gen[set[int]] + $use[B]] ] + 'end" + */); +} diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/SavedQuestions.txt b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/SavedQuestions.txt new file mode 100644 index 00000000000..65e54b63b10 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/SavedQuestions.txt @@ -0,0 +1,1769 @@ +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/Concatenation/Concatenation.concept| +QChoice: When you compute the concatenation of two lists, the number of elements in the result is always: +b: Greater than the sum of the number of elements in both lists. +g: Greater than or equal to the sum of the number of elements in both lists. +b: Smaller than the sum of the number of elements in both lists. +g: Equal to the sum of the number of elements in both lists. + +QType: <A:list[arb[int,str]]> + <B:same[A]> + +QValue: <A:list[arb[int,str]]> + <B:same[A]> + +QValue: +make: A = list[arb[int,str]] +make: B = same[A] +expr: C = <A> + <B> +hint: <B> +test: <A> + <?> == <C> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/notin/notin.concept| +QType: <A:arb[int,str,bool]> notin <B:list[same[A]]> + + +QValue: +prep: import Set; +make: ELM = int[0,100] +make: A = set[same[ELM]] +expr: A1 = toList( {<ELM>} + <A>) +expr: C = <ELM> notin <A1> +hint: <C> +test: <ELM> notin <A1> == <?> + +QValue: +prep: import Set; +make: ELM = int[0,10] +make: A = list[same[ELM]] +expr: C = <ELM> notin <A> +hint: <C> +test: <ELM> notin <A> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/ListRelation/Composition/Composition.concept| +QType: +prep: import List; +make: C = list[int,3,3] +make: A = list[int,3,3] +make: B = list[int,3,3] +expr: S1 = zip(<A>,<C>) +expr: S2 = zip(<C>, <B>) +test: <S1> o <S2> + +QValue: +prep: import List; +make: C = list[int,3,3] +make: A = list[int,3,3] +make: B = list[int,3,3] +expr: S1 = zip(<A>,<C>) +expr: S2 = zip(<C>, <B>) +expr: H = <S1> o <S2> +hint: <H> +test: <S1> o <S2> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Relation/Composition/Composition.concept| +QType: +prep: import List; +make: C = list[int,3,3] +make: A = list[int,3,3] +make: B = list[int,3,3] +expr: S1 = toSet(zip(<A>,<C>)) +expr: S2 = toSet(zip(<C>, <B>)) +test: <S1> o <S2> + +QValue: +prep: import List; +make: C = list[int,3,3] +make: A = list[int,3,3] +make: B = list[int,3,3] +expr: S1 = toSet(zip(<A>,<C>)) +expr: S2 = toSet(zip(<C>, <B>)) +expr: H = <S1> o <S2> +hint: <H> +test: <S1> o <S2> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/SuperList/SuperList.concept| +QType: <A:list[arb]> >= <B:same[A]> + + +QValue: +prep: import Set; +make: DIFF = set[int[0,100]] +make: B = same[DIFF] +expr: A = toList(<B> + <DIFF>) +expr: B1 = toList(<B>) +expr: C = <A> >= <B1> +hint: <C> +test: <A> >= <B1> == <?> + +QValue: +make: A = list[arb[int,str,bool]] +expr: C = <A> >= <A> +hint: <C> +test: <A> >= <A> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/Equal/Equal.concept| +QType: <A:int> == <B:int> +QType: <A:real> == <B:real> +QType: <A:num> == <B:num> +QType: <A:num> == <B:num> +QValue: <A:num> == <B:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/Subtraction/Subtraction.concept| +QType: <A:int> - <B:int> +QType: <A:int> - <B:real> +QType: <A:real> - <B:int> +QValue: <A:num> - <B:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Map/Map.concept| +QChoice: A map maps keys to values. In a map: +g: All keys have to be unique. +g: All keys have the same type. +g: All keys should have the same type and all values should have the same type. The type of keys and values may be different. +b: All keys should have the same type and all values should have the same type. The type of keys and values should be the same. +b: All values have to be unique. +b: All keys and values have to be unique. +b: All keys are sorted. + +QValue: +desc: Complete this function that returns the set of keys with the smallest associated value. +list: +import Map; +import Set; +inventory = ("orange" : 20, "apple" : 15, "banana" : 25, "lemon" : 15); +public set[str] lowest(map[str,int] inv){ + m = <?>; // Determine the minimal value in the map + return { s | s <- inv, inv[s] == m }; +} +test: lowest(inventory) == {"apple", "lemon"}; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Patterns/Abstract/List/List.concept| +QValue: +desc: Complete this function that tests that a list of words consists of two identical sublists: +list: +import List; +public bool isReplicated(list[str] words){ + return [*str L, <?>] := words; +} +test: isReplicated(["a", "b", "a", "b"]) == true; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/Remainder/Remainder.concept| +QType: <A:int> % <B:int> +QValue: <A:int> % <B:int> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Libraries/Prelude/List/List.concept| +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],4,6] +make: I = int[0,3] +expr: E = <L>[<I>] +expr: H = indexOf(<L>, <E>) +hint: indexOf +test: <?>(<L>, <E>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: E = arb[int,str] +make: L = list[same[E],4,6] +make: I = int[0,3] +expr: H = insertAt(<L>, <I>, <E>) +hint: insertAt +test: <?>(<L>, <I>, <E>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[int,0,5] +expr: H = intercalate(";", <L>) +hint: intercalate +test: <?>(";", <L>) == <H> + + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],0,5] +expr: H = index(<L>) +hint: index +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],1,6] +expr: H = head(<L>) +hint: head +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str], 1, 6] +expr: H = last(<L>) +hint: last +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str]] +expr: H = toSet(<L>) +hint: toSet +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],4,6] +make: N = int[0,3] +expr: H = take(<N>, <L>) +hint: take +test: <?>(<N>, <L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],4,6] +make: N = int[0,3] +expr: H = tail(<L>,<N>) +hint: tail +test: <?>(<L>,<N>) == <H> + + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],1,5] +expr: H = headTail(<L>) +hint: Use headTail. +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],4,6] +make: I = int[0,3] +make: J = int[0,3] +expr: L1 = [<L>[<I>], <L>[<J>], *<L>, <L>[<J>], <L>[<I>]] +expr: H = dup(<L1>) +hint: dup +test: <?>(<L1>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],2,5] +expr: H = getOneFrom(<L>) +hint: getOneFrom +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],3,4] +make: N = int[0,2] +expr: H = drop(<N>, <L>) +hint: drop +test: <?>(<N>, <L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],0,5] +expr: H = reverse(<L>) +hint: reverse +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],3,4] +make: I = int[0,2] +expr: E = <L>[<I>] +expr: L1 = reverse(<L>) + <L> +expr: H = lastIndexOf(<L1>, <E>) +hint: lastIndexOf +test: <?>(<L1>, <E>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],0,5] +expr: H = index(<L>) +hint: index +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],1,5] +expr: H = pop(<L>) +hint: pop +test: <?>(<L>) == <H> + + +QValue: +prep: import List; +make: L = list[arb[int,str], 1, 6] +expr: H = max(<L>) +hint: max +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[int[-20,20]] +expr: H = takeWhile(<L>, bool(int x){ return x > 0;}) +hint: takeWhile +test: <?>(<L>, bool(int x){ return x > 0;}) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],3,4] +make: I = int[0,2] +expr: C = delete(<L>, <I>) +hint: delete +test: <?>(<L>, <I>) == <C> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[int,2,7] +expr: H = sum(<L>) +hint: sum +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[int, 4, 4] +make: M = list[int, 4, 4] +expr: Z = zip(<L>,<M>) +hint: zip +test: <?>(<L>, <M>) == <Z> + + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],1,5] +expr: H = sort(<L>) +hint: sort +test: <?>(<L>) == <H> + + +QValue: +prep: import List; +make: L = list[int, 1,5] +expr: H = mapper(<L>, int(int n){ return n + 1; }) +hint: mapper +list: +int incr(int x) { return x + 1; } +test: <?>(<L>, incr) == <H> + +QValue: +desc: Complete this function that tests that a list of words forms a palindrome. A palindrome is a word that is symmetrical +and can be read +from left to right and from right to left. +list: +import List; +public bool isPalindrome(list[str] words){ + return words == <?>; +} +test: isPalindrome(["a", "b", "b", "a"]) == true; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/Conditional/Conditional.concept| +QType: (<A:int> > <B:int>) ? <C:int> : <D:int> + +QType: (<A:int> > <B:int>) ? <C:real> : <D:real> + +QType: (<A:int> > <B:int>) ? <C:int> : <D:real> + +QValue: (<A:int> > <B:int>) ? <C:int> : <D:int> + +QValue: (<A:int> > <B:int>) ? <C:int> : <D:real> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/Equal/Equal.concept| +QType: <A:set[arb]> == <B:same[A]> + +QValue: +make: A = set[arb[int[0,10],str]] +expr: C = <A> == <A> +hint: <C> +test: (<A> == <A>) == <?> + +QValue: +make: A = set[int[0,100]] +make: DIFF = int +expr: B = <A> + (<DIFF>) +expr: C = <A> == <B> +hint: <C> +test: (<A> == <B>) == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/Comprehension/Comprehension.concept| +QType: +make: I = int[0,5] +make: J = int[7,12] +test: { N + 1 | int N <- [<I> .. <J>] } + +QValue: +make: I = int[0,5] +make: J = int[7,12] +expr: H = { N + 1 | int N <- [<I> .. <J>] } +hint: <H> +test: { N + 1 | int N <- [<I> .. <J>] } == <?> + +QValue: +make: S = set[int] +expr: H = { 2 * N | int N <- <S> } +hint: <H> +test: { 2 * N | int N <- <S> } == <?> + +QValue: +desc: Complete this comprehension: +make: S = set[int] +expr: H = { N - 1 | int N <- <S> } +hint: { N - 1 | int N <- <S> } +test: { <?> | int N <- <S> } == <H> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Integer/Integer.concept| +QType: <A:int> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/LessThanOrEqual/LessThanOrEqual.concept| +QType: <A:int> <= <B:int> +QType: <A:real> <= <B:real> +QType: <A:num> <= <B:num> +QType: <A:num> <= <B:num> +QValue: <A:num> <= <B:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/ListRelation/Subscription/Subscription.concept| +QValue: +desc: Using the above example with GDP values: +prep: lrel[str country, int year, int amount] GDP = [<"US", 2008, 14264600>, <"EU", 2008, 18394115>,<"Japan", 2008, 4923761>, <"US", 2007, 13811200>, <"EU", 2007, 13811200>, <"Japan", 2007, 4376705>]; +expr: H = GDP["US"] +hint: <H> +test: GDP["US"] == <?> + +QValue: +desc: Using the above example with GDP values: +prep: lrel[str country, int year, int amount] GDP = [<"US", 2008, 14264600>, <"EU", 2008, 18394115>,<"Japan", 2008, 4923761>, <"US", 2007, 13811200>, <"EU", 2007, 13811200>, <"Japan", 2007, 4376705>]; +expr: H = GDP["US",2008] +hint: <H> +test: GDP["US",2008] == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/ListRelation/FieldSelection/FieldSelection.concept| +QValue: +hint: A list consisting of the first element of each tuple. +list: +lrel[str animal, int nlegs] legs = [<"bird", 2>, <"dog", 4>, <"human", 2>, <"snake", 0>, <"spider", 8>, <"millepede", 1000>, <"crab", 8>, <"cat", 4>]; +test: legs.animal == <?> + +QValue: +hint: A list consisting of the second element of each tuple. +list: +lrel[str animal, int nlegs] legs = [<"bird", 2>, <"dog", 4>, <"human", 2>, <"snake", 0>, <"spider", 8>, <"millepede", 1000>, <"crab", 8>, <"cat", 4>]; +test: legs.nlegs == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/ListRelation/ReflexiveTransitiveClosure/ReflexiveTransitiveClosure.concept| +QType: +prep: S = [<1,2>, <2,3>]; +make: A = list[tuple[int[0,5],int[0,5]],1,2] +expr: B = <A> + S +test: <B>* + +QValue: +prep: S = [<1,2>, <2,3>, <3,4>]; +make: A = list[tuple[int[0,5],int[0,5]],1,2] +expr: B = <A> + S +expr: H = <B>* +hint: <H> +test: <B>* +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/ListRelation/Join/Join.concept| +QType: <A:list[tuple[int,str]]> join <B:list[tuple[str,int]]> +QValue: <A:list[tuple[int,str],2,2]> join <B:list[tuple[str,int],2,2]> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Visit/Visit.concept| +QValue: +desc: Given a data type `ColoredTree`, complete the definition of the function `flipRedChildren` that exchanges the children of all red nodes. +list: +data ColoredTree = leaf(int N) + | red(ColoredTree left, ColoredTree right) + | black(ColoredTree left, ColoredTree right); + +ColoredTree rb = red(black(leaf(1), red(leaf(2),leaf(3))), black(leaf(3), leaf(4))); + +public ColoredTree flipRedChildren(ColoredTree t){ + return visit(t){ + case red(l,r) => <?> + }; +} +test: flipRedChildren(rb) == red( black(leaf(3), leaf(4)), black(leaf(1), red(leaf(3),leaf(2)))); +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/ListRelation/CartesianProduct/CartesianProduct.concept| +QType: +make: S1 = list[int,2,3] +make: S2 = list[int,2,3] +test: <S1> * <S2> + +QValue: +make: S1 = list[int,2,3] +make: S2 = list[int,2,2] +expr: H = <S1> * <S2> +hint: <H> +test: <S1> * <S2> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Relation/Relation.concept| +QChoice: A relation: +g: Is a set of tuples. +b: Is a list of tuples. +b: Is a tuple of tuples. +b: Has ordered elements. +b: Can contain duplicates. +b: Has a fixed length. +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/ListRelation/ListRelation.concept| +QChoice: A relation: +g: Is a set of tuples. +b: Is a list of tuples. +b: Is a tuple of tuples. +b: Has ordered elements. +b: Can contain duplicates. +b: Has a fixed length. +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/List.concept| +QChoice: The type of a list is determined by: +g: The least upper bound of the type of all elements. +g: The types of all the elements in the list. +b: The type of the element that was first added to the list. +b: The average of the type of the elements with the smallest and the largest type. +b: The least upper bound of the type of two arbitrary elements. +b: The type of two arbitrary elements. + + +QValue: +desc: Fill in the missing operator. +make: B = arb[int[0,100],str] +make: A = list[same[B]] +expr: C = <A> + <B> +hint: Use +. +test: <A> <?> <B> == <C> + +QValue: +desc: Fill in the missing operator. +prep: import Set; +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = toList(<DIFF> + <A>) +expr: B1 = toList(<B> + <DIFF>) +expr: C = <A1> - <B1> +hint: Use -. +test: <A1> <?> <B1> == <C> + +QValue: +desc: Fill in the missing operator. +prep: import List; +make: A = list[int[0,100]] +expr: B = reverse(<A>) +expr: C = <A> == <B> +hint: Use ==. +test: (<A> <?> <B>) == <C> + +QValue: +desc: Fill in the missing operator. +prep: import Set; +make: DIFF = set[int[0,100],str] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = toList(<DIFF> + <A>) +expr: B1 = toList(<B> + <DIFF>) +expr: C = <A1> & <B1> +hint: Use &. +test: <A1> <?> <B1> == <C> + +QValue: +desc: Fill in the missing operator. +prep: import Set; +make: ELM = int[0,100] +make: A = set[same[ELM]] +expr: A1 = toList( {<ELM>} + <A>) +expr: C = <ELM> in <A1> +hint: Use in. +test: <ELM> <?> <A1> == <C> + +QValue: +desc: Fill in the missing operator. +prep: import Set; +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +expr: A1 = toList(<A>) +expr: B = toList(<A> + <DIFF>) +expr: C = <A1> < <B> +hint: <C> +test: <A1> <?> <B> == <C> + + +QValue: +desc: Fill in the missing operator. +make: A = list[arb[int,str]] +make: B = same[A] +expr: C = <A> + <B> +hint: Use +. +test: <A> <?> <B> == <C> + +QValue: +desc: Fill in the missing operator. +prep: import Set; +make: ELM = int[0,10] +make: A = list[same[ELM]] +expr: C = <ELM> notin <A> +hint: Use notin. +test: <ELM> <?> <A> == <C> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Real/Real.concept| +QType: <A:real> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Tutor/Markup/QuestionMarkup/Text/Text.concept| +QText[Taller]: What is taller, the Eiffel Tower or the Empire State Building? +a: Empire +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Tutor/Markup/QuestionMarkup/Type/Type.concept| +QType: <A:set[int]> + +QType: <A:set[arb[int,str,real]]> + <B:same[A]> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Relation/FieldSelection/FieldSelection.concept| +QValue: +hint: A set consisting of the first element of each tuple. +list: +rel[str animal, int nlegs] legs = {<"bird", 2>, <"dog", 4>, <"human", 2>, <"snake", 0>, <"spider", 8>, <"millepede", 1000>, <"crab", 8>, <"cat", 4>}; +test: legs.animal == <?> + +QValue: +hint: A set consisting of the second element of each tuple. +list: +rel[str animal, int nlegs] legs = {<"bird", 2>, <"dog", 4>, <"human", 2>, <"snake", 0>, <"spider", 8>, <"millepede", 1000>, <"crab", 8>, <"cat", 4>}; +test: legs.nlegs == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Relation/ReflexiveTransitiveClosure/ReflexiveTransitiveClosure.concept| +QType: +prep: S = {<1,2>, <2,3>}; +make: A = set[tuple[int[0,5],int[0,5]],1,2] +expr: B = <A> + S +test: <B>* + +QValue: +prep: S = {<1,2>, <2,3>, <3,4>}; +make: A = set[tuple[int[0,5],int[0,5]],1,2] +expr: B = <A> + S +expr: H = <B>* +hint: <H> +test: <B>* +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Relation/Join/Join.concept| +QType: <A:set[tuple[int,str]]> join <B:set[tuple[str,int]]> +QValue: <A:set[tuple[int,str],2,2]> join <B:set[tuple[str,int],2,2]> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Reducer/Reducer.concept| +QValue: +desc: Return the set of largest words. +list: +import Number; +import String; +text = ["Quote", "from", "Steve", "Jobs", ":", "And", "one", "more", "thing"]; +public list[str] largest(list[str] text){ + mx = ( 0 | max(it, size(s)) | s <- text ); + return + for(s <- text) + if(<?>) + append s; +} +test: largest(text) == ["Quote", "Steve", "thing"]; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Relation/Subscription/Subscription.concept| +QValue: +desc: Using the above example with GDP values: +prep: rel[str country, int year, int amount] GDP = {<"US", 2008, 14264600>, <"EU", 2008, 18394115>,<"Japan", 2008, 4923761>, <"US", 2007, 13811200>, <"EU", 2007, 13811200>, <"Japan", 2007, 4376705>}; +expr: H = GDP["US"] +hint: <H> +test: GDP["US"] == <?> + +QValue: +desc: Using the above example with GDP values: +prep: rel[str country, int year, int amount] GDP = {<"US", 2008, 14264600>, <"EU", 2008, 18394115>,<"Japan", 2008, 4923761>, <"US", 2007, 13811200>, <"EU", 2007, 13811200>, <"Japan", 2007, 4376705>}; +expr: H = GDP["US",2008] +hint: <H> +test: GDP["US",2008] == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/SubList/SubList.concept| +QType: <A:list[arb]> <= <B:same[A]> + +QValue: +prep: import Set; +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +expr: A1 = toList(<A>) +expr: B = toList(<A> + <DIFF>) +expr: C = <A1> <= <B> +hint: <C> +test: <A1> <= <B> == <?> + +QValue: +make: A = list[arb[int,str,bool]] +expr: C = <A> <= <A> +hint: <C> +test: <A> <= <A> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Tutor/Markup/QuestionMarkup/Choice/Choice.concept| +QChoice[Faster]: Which means of transportation is faster? +b: Apache Helicopter +g: High speed train +b: Ferrari F430 +b: Hovercraft +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Relation/CartesianProduct/CartesianProduct.concept| +QType: +make: S1 = set[int,2,3] +make: S2 = set[int,2,3] +test: <S1> * <S2> + +QValue: +make: S1 = set[int,2,3] +make: S2 = set[int,2,2] +expr: H = <S1> * <S2> +hint: <H> +test: <S1> * <S2> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/RascalTests/TestSoftwareEvolution/TestSoftwareEvolution.concept| +QChoice: Sets can be used to represent a sequence of values when +b: The values have duplicates. +g: The values have no duplicates and no order. +b: The values are unordered. + +QChoice: The type of a list is determined by: +b: The type of the first element that was first added to the list. +b: The upperbound of the type of two arbitrary elements. +g: The upperbound of the type of all elements. + +QType: <A:set[arb[int,real,str,loc]]> + +QType: <A:list[arb[int,real,str,loc]]> + +QType: <A:map[str,arb]> + + +QType: +make: A = int +type: set[int] +test: {<A>, <?> } +hint: one or more integer values separated by commas + +QType: +make: A = str +type: map[str,int] +test: (<A>: <?>) +hint: a map from strings to integers + +QType: <A:set[arb[int,real,num,str,loc]]> + +QType: {<A:int>, <B:str>, <C:int>} + +QType: <A:rel[str,int,loc]> + +QType: <A:rel[int[0,20],int]> + +QValue: +desc: Determine the number of elements in a list +list: +import List; +text = ["abc", "def", "ghi"]; +test: <?>(text) == 3; + +QValue: +desc: Determine the number of strings that contain "a". +list: +text = ["andra", "moi", "ennepe", "Mousa", "polutropon"]; +public int count(list[str] text){ + n = 0; + for(s <- text) + if(<?> := s) + n +=1; + return n; +} + +test: count(text) == 2; + +QValue: +desc: Return the strings that contain "o". +list: +text = ["andra", "moi", "ennepe", "Mousa", "polutropon"]; +public list[str] find(list[str] text){ + return + for(s <- text) + if(/o/ := s) + <?>; +} +test: find(text) == ["moi", "Mousa", "polutropon"]; + +QValue: +desc: Complete this function that finds duplicates in a list of strings +list: +text = ["the", "jaws", "that", "bite", "the", "claws", "that", "catch"]; +public list[str] duplicates(list[str] text){ + m = {}; + return + for(s <- text) + if(<?>) + append s; + else + m += s; +} +test: duplicates(text) == ["the", "that"]; + +QValue: +desc: Complete this function that tests that a list of words forms a palindrome. A palindrome is a word that is symmetrical +and can be read +from left to right and from right to left. +list: +import List; +public bool isPalindrome(list[str] words){ + return words == <?>; +} +test: isPalindrome(["a", "b", "b", "a"]) == true; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/Difference/Difference.concept| +QChoice: When you compute the difference of two lists, the number of elements in the result is always: +b: Greater than the number of elements in the first list. +b: Greater than the number of elements in the second list. +b: Greater than or equal to the sum of the number of elements in both lists. +g: Smaller than or equal to the number of elements in the first list. +b: Smaller than or equal to the number of elements in the second list. +b: Equal to the sum of the number of elements in both lists. + +QType: +make: A = list[arb[int[0,100],str]] +make: B = same[A] +test: <A> - <B> + +QValue: +prep: import Set; +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = toList(<DIFF> + <A>) +expr: B1 = toList(<B> + <DIFF>) +expr: C = <A1> - <B1> +hint: <C> +test: <A1> - <B1> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/Product/Product.concept| +QChoice: When you compute the product of two lists, the number of elements in the result is always: +b: Smaller than or equal to the number of elements in the first list. +b: Smaller than or equal to the number of elements in the second list. +g: Equal to the product of the number of elements in both lists. +b: Equal to the sum of the number of elements in both lists. +b: Equal to the number of elements in the largest list. + + +QType: <A:list[arb[int,str,bool]]> * <B:same[A]> + +QValue: +make: A = list[int[0,50]] +make: B = int[0,50] +expr: C = <A> * [<B>] +hint: <C> +test: <A> * [<B>] == <?> + +QValue: +make: A = list[int[0,50]] +make: B = int[0,50] +make: C = int[0,50] +expr: D = <A> * [<B>, <C>] +hint: <D> +test: <A> * [<B>, <C>] == <?> + +QValue: +prep: import List; +make: A = list[int[0,50]] +make: B = same[A] +expr: C = size(<A> * <B>) +hint: <C> +test: size(<A> * <B>) == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/NotEqual/NotEqual.concept| +QType: <A:list[arb]> != <B:same[A]> + +QValue: +make: A = list[arb[int[0,10],str]] +expr: C = <A> != <A> +hint: <C> +test: (<A> != <A>) == <?> + +QValue: +make: A = list[int[0,100],str] +make: B = same[A] +expr: C = <A> != <B> +hint: <C> +test: (<A> != <B>) == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/Intersection/Intersection.concept| +QChoice: When you compute the intersection of two lists, the number of elements in the result is always: +b: Greater than the number of elements in the first list. +b: Greater than the number of elements in the second list. +b: Greater than or equal to the sum of the number of elements in both lists. +g: Smaller than or equal to the number of elements in the first list. +b: Smaller than or equal to the number of elements in the second list. +b: Equal to the sum of the number of elements in both lists. + +QType: <A:list[int[0,100],str]> & <B:same[A]> + +QValue: +prep: import Set; +make: DIFF = set[int[0,100],str] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = toList(<DIFF> + <A>) +expr: B1 = toList(<B> + <DIFF>) +expr: C = <A1> & <B1> +hint: <C> +test: <A1> & <B1> == <?> + +QValue: +make: A = list[int[0,10],str] +make: B = same[A] +expr: C = <A> & <B> +hint: <C> +test: <A> & <B> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/Insert/Insert.concept| +QChoice: When you insert an element in a list, the number of elements in the result is always: +g: Greater than the number of elements in the original list. +g: One larger than the number of elements in the original list. +b: Smaller than the number of elements in the original list. +b: One smaller than the number of elements in the original list. + +QType: +make: A = arb[int[0,100],str] +make: B = list[same[A]] +test: <A> + <B> + +QValue: +make: A = arb[int[0,100],str] +make: B = list[same[A]] +hint: <A> + <B> +test: <A> + <B> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/Subscription/Subscription.concept| +QChoice: For a list of length $N$, legal index value are: +g: The integers 0, 1, ..., N - 1. +b: Positive integers. +b: The integers 1, 2, ..., N. +b: Even integers. +b: Odd integers. +b: The integers, 0, 2, ..., N. + +QValue: +prep: import List; +make: L = list[arb[bool,int,str],4,6] +make: I = int[0,3] +expr: C = <L>[<I>] +hint: <C> +list: +L = <L>; +test: L[<I>] == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/in/in.concept| +QType: <A:arb[int,str,bool]> in <B:list[same[A]]> + +QValue: +prep: import Set; +make: ELM = int[0,100] +make: A = set[same[ELM]] +expr: A1 = toList( {<ELM>} + <A>) +expr: C = <ELM> in <A1> +hint: <C> +test: <ELM> in <A1> == <?> + +QValue: +prep: import Set; +make: ELM = int[0,10] +make: A = list[same[ELM]] +expr: C = <ELM> in <A> +hint: <C> +test: <ELM> in <A> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/Append/Append.concept| +QChoice: When you append an element to a list, the number of elements in the result is always: +g: Greater than the number of elements in the original list. +g: One larger than the number of elements in the original list. +b: Smaller than the number of elements in the original list. +b: One smaller than the number of elements in the original list. + +QType: +make: B = arb[int[0,100],str] +make: A = list[same[B]] +test: <A> + <B> + +QValue: +make: B = arb[int[0,100],str] +make: A = list[same[B]] +expr: C = <A> + <B> +hint: <C> +test: <A> + <B> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Boolean/Match/Match.concept| +QValue: +desc: Determine the number of strings that contain "a". +list: +text = ["andra", "moi", "ennepe", "Mousa", "polutropon"]; +public int count(list[str] text){ + n = 0; + for(s <- text) + if(<?> := s) + n +=1; + return n; +} +test: count(text) == 2; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Tutor/Markup/QuestionMarkup/Value/Value.concept| +QValue: <A:set[int]> + <B:same[A]> + +QValue: +prep: import List; +test: size(<A:list[int]>) == <?> + +QValue: +make: A = set[arb[int,str]] +make: B = same[A] +expr: C = <A> + <B> +hint: <B> +test: <A> + <?> == <C> + +QValue: +desc: Return the strings that contain "o". +list: +text = ["andra", "moi", "ennepe", "Mousa", "polutropon"]; +public list[str] find(list[str] text){ + return + for(s <- text) + if(/o/ := s) + <?>; +} +test: find(text) == ["moi", "Mousa", "polutropon"]; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/notin/notin.concept| +QType: <A:arb[int,str,bool]> notin <B:set[same[A]]> + + +QValue: +make: ELM = int[0,100] +make: A = set[same[ELM]] +expr: A1 = {<ELM>, *<A>} +expr: C = <ELM> notin <A1> +hint: <C> +test: <ELM> notin <A1> == <?> + +QValue: +make: ELM = int[0,10] +make: A = set[same[ELM]] +expr: C = <ELM> notin <A> +hint: <C> +test: <ELM> notin <A> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/StrictSuperList/StrictSuperList.concept| +QType: <A:list[arb]> > <B:same[A]> + +QValue: +prep: import Set; +make: DIFF = set[int[0,100]] +make: B = same[DIFF] +expr: A = toList(<B> + <DIFF>) +expr: B1 = toList(<B>) +expr: C = <A> > <B1> +hint: <C> +test: <A> > <B1> == <?> + +QValue: +make: A = list[arb[int,str,bool]] +expr: C = <A> > <A> +hint: <C> +test: <A> > <A> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/Set.concept| +QChoice: Sets can be used to represent a sequence of values when +b: The values have duplicates. +g: The values have no duplicates and no order. +b: The values are unordered. + +QChoice: The type of a set is determined by: +g: The least upper bound of the type of all elements. +g: The types of all the elements in the set. +b: The type of the element that was first added to the set. +b: The average of the type of the elements with the smallest and the largest type. +b: The least upper bound of the type of two arbitrary elements. +b: The type of two arbitrary elements. + + +QType: {1, <?> } +type: set[int] +hint: one or more integer values separated by commas + +QType: <A:set[arb[int,str]]> + +QType: {<A:int>, <B:str>, <C:int>} + +QValue: +desc: Fill in the missing operator. +make: ELM = int[0,100] +make: A = set[same[ELM]] +expr: A1 = {<ELM>} + <A> +expr: C = <ELM> in <A1> +hint: in +test: <ELM> <?> <A1> == <C> + +QValue: +desc: Fill in the missing operator. +make: A = arb[int[0,100],str] +make: B = set[same[A]] +expr: H = <A> + <B> +hint: + +test: <A> <?> <B> == <H> + +QValue: +desc: Fill in the missing operator. +make: DIFF = set[int[0,100],str] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = <DIFF> + <A> +expr: B1 = <B> + <DIFF> +expr: C = <A1> & <B1> +hint: & +test: <A1> <?> <B1> == <C> + + +QValue: +desc: Fill in the missing operator. +prep: import Set; +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = <DIFF> + <A> +expr: B1 = <B> + <DIFF> +expr: C = <A1> - <B1> +hint: - +test: <A1> <?> <B1> == <C> + +QValue: +desc: Fill in the missing operator. +make: ELM = int[0,10] +make: A = set[same[ELM]] +expr: C = <ELM> notin <A> +hint: notin +test: <ELM> <?> <A> == <C> + +QValue: +desc: Fill in the missing operator. +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +expr: B = <A> + <DIFF> +expr: C = <A> < <B> +hint: < +test: <A> <?> <B> == <C> + +QValue: +desc: Fill in the missing operator. +make: A = set[arb[int,str]] +make: B = same[A] +expr: C = <A> + <B> +hint: + +test: <A> <?> <B> == <C> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/GreaterThan/GreaterThan.concept| +QType: <A:int> > <B:int> +QType: <A:real> > <B:real> +QType: <A:num> > <B:num> +QType: <A:num> > <B:num> +QValue: <A:num> > <B:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Patterns/Regular/Regular.concept| +QValue: +desc: Return the strings that contain "o". +list: +text = ["andra", "moi", "ennepe", "Mousa", "polutropon"]; +public list[str] find(list[str] text){ + return + for(s <- text) + if(/o/ := s) + <?>; +} +test: find(text) == ["moi", "Mousa", "polutropon"]; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/ListRelation/TransitiveClosure/TransitiveClosure.concept| +QType: +prep: S = [<1,2>, <2,3>]; +make: A = list[tuple[int[0,5],int[0,5]],1,2] +expr: B = <A> + S +test: <B>+ + +QValue: +prep: S = [<1,2>, <2,3>, <3,4>]; +make: A = list[tuple[int[0,5],int[0,5]],1,2] +expr: B = <A> + S +expr: H = <B>+ +hint: <H> +test: <B>+ +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/Equal/Equal.concept| +QType: <A:list[arb]> == <B:same[A]> + +QValue: +make: A = list[arb[int[0,10],str]] +expr: C = <A> == <A> +hint: <C> +test: (<A> == <A>) == <?> + +QValue: +prep: import List; +make: A = list[int[0,100]] +expr: B = reverse(<A>) +expr: C = <A> == <B> +hint: <C> +test: (<A> == <B>) == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/Comprehension/Comprehension.concept| +QValue: +desc: Return the strings that contain a given substring. +list: +text = ["An", "honest", "man", "is", "always", "a", "child"]; +public list[str] find(list[str] text, str contains) = [ s | s <- text, <?> ]; +test: find(text, "n") == ["An", "honest", "man"]; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/LessThan/LessThan.concept| +QType: <A:int> < <A:int> +QType: <A:real> < <B:real> +QType: <A:num> < <B:num> +QType: <A:num> < <B:num> +QValue: <A:num> < <B:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/Negation/Negation.concept| +QType: -<A:int> +QType: -<A:real> +QValue: -<A:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/Multiplication/Multiplication.concept| +QType: <A:int> * <B:int> +QType: <A:int> * <B:real> +QType: <A:real> * <B:int> +QValue: <A:num> * <B:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Statements/If/If.concept| +QType: if( <A:int> > <B:int> ) 10; else 20; + +QType: if( <A:int> > <B:int> ) <C:str>; else <D:str>; + +QValue: if( <A:int> > <B:int> ) 10; else 20; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/Division/Division.concept| +QType: <A:int> / <B:int> +QType: <A:int> / <B:real> +QType: <A:real> / <B:int> +QValue: <A:num> / <B:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Statements/Append/Append.concept| +QValue: +desc: Complete this function that finds duplicates in a list of strings +list: +text = ["the", "jaws", "that", "bite", "the", "claws", "that", "catch"]; +public list[str] duplicates(list[str] text){ + m = {}; + return + for(s <- text) + if(<?>) + append s; + else + m += s; +} +test: duplicates(text) == ["the", "that"]; +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/Addition/Addition.concept| +QType: <A:int[0]> + <B:int[0]> +desc: Adding integers. + +QValue: <A:int[0]> + <B:int[0]> +desc: Adding integers. + +QType:<A:int[0]> + <B:real[0]> +desc: Adding integers and reals. + +QValue:<A:int[0]> + <B:real[0]> +Adding integers and reals. + +QValue: <A:int> + (<B:int[-20,-1]>) +desc:Use parentheses when addition and negative numbers interact. + +QValue: +desc: Use parentheses when addition and negative numbers interact. +make: A = int +make: B = int[0,10] +expr: C = <A> - <B> +test: <A> + <?> == <C> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/GreaterThanOrEqual/GreaterThanOrEqual.concept| +QType: <A:int> >= <B:int> +QType: <A:real> >= <B:real> +QType: <A:num> >= <B:num> +QType: <A:num> >= <B:num> +QValue: <A:num> >= <B:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Number/NotEqual/NotEqual.concept| +QType: <A:int> != <B:int> +QType: <A:real> != <B:real> +QType: <A:num> != <B:num> +QType: <A:num> != <B:num> +QValue: <A:num> != <B:num> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/List/StrictSubList/StrictSubList.concept| +QType: <A:list[arb]> < <B:same[A]> + +QValue: +prep: import Set; +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +expr: A1 = toList(<A>) +expr: B = toList(<A> + <DIFF>) +expr: C = <A1> < <B> +hint: <C> +test: <A1> < <B> == <?> + +QValue: +make: A = list[arb[int,str,bool]] +expr: C = <A> < <A> +hint: <C> +test: <A> < <A> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Relation/TransitiveClosure/TransitiveClosure.concept| +QType: +prep: S = {<1,2>, <2,3>}; +make: A = set[tuple[int[0,5],int[0,5]],1,2] +expr: B = <A> + S +test: <B>+ + +QValue: +prep: S = {<1,2>, <2,3>, <3,4>}; +make: A = set[tuple[int[0,5],int[0,5]],1,2] +expr: B = <A> + S +expr: H = <B>+ +hint: <H> +test: <B>+ +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Tuple/Tuple.concept| +QChoice: For a tuple: +g: All elements may have different types. +b: All elements should have the same type. +g: It's type changes with the number of elements. +b: It's type does not change with the number of elements. +g: The order of the elements is relevant. +b: The order of the elements is not relevant. +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/Difference/Difference.concept| +QChoice: When you compute the difference of two sets, the number of elements in the result is always: +b: Greater than the number of elements in both sets. +b: Greater than or equal to the number of elements in the first set. +b: Smaller than the number of elements in both sets. +g: Smaller than or equal to the number of elements in the first set. + +QType: <A:set[arb[int,str]]> - <B:same[A]> + +QType: <A:set[arb[str,int]]> - <A:same[A]> + +QValue: +prep: import Set; +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = <DIFF> + <A> +expr: B1 = <B> + <DIFF> +expr: C = <A1> - <B1> +hint: <C> +test: <A1> - <B1> == <?> + +QValue: +prep: import Set; +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = <DIFF> + <A> +expr: B1 = <B> + <DIFF> +expr: C = <A1> - <B1> +hint: <A1> +test: <?> - <B1> == <C> + +QValue: +prep: import Set; +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = <DIFF> + <A> +expr: B1 = <B> + <DIFF> +expr: C = <A1> - <B1> +hint: <B1> +test: <A1> - <?> == <C> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/NotEqual/NotEqual.concept| +QType: <A:list[arb]> != <B:same[A]> + +QValue: +make: A = set[arb[int[0,10],str]] +expr: C = <A> != <A> +hint: <C> +test: (<A> != <A>) == <?> + +QValue: +make: A = set[int[0,100],str] +make: B = same[A] +expr: C = <A> != <B> +hint: <C> +test: (<A> != <B>) == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/StrictSuperSet/StrictSuperSet.concept| +QType: <A:list[arb]> > <B:same[A]> + +QValue: +make: DIFF = set[int[0,100]] +make: B = same[DIFF] +expr: A = <B> + <DIFF> +expr: C = <A> > <B> +hint: <C> +test: <A> > <B> == <?> + +QValue: +make: A = set[arb[int,str,bool]] +expr: C = <A> > <A> +hint: <C> +test: <A> > <A> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/StrictSubSet/StrictSubSet.concept| +QType: <A:set[arb[int,str]]> < <B:same[A]> + +QValue: +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +expr: B = <A> + <DIFF> +expr: C = <A> < <B> +hint: <C> +test: <A> < <B> == <?> + +QValue: +make: A = set[arb[int,str,bool]] +expr: C = <A> < <A> +hint: <C> +test: <A> < <A> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/SuperSet/SuperSet.concept| +QType: <A:set[arb]> >= <B:same[A]> + + +QValue: +make: DIFF = set[int[0,100]] +make: B = same[DIFF] +expr: A = <B> + <DIFF> +expr: C = <A> >= <B> +hint: <C> +test: <A> >= <B> == <?> + +QValue: +make: A = set[arb[int,str,bool]] +expr: C = <A> >= <A> +hint: <C> +test: <A> >= <A> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/Product/Product.concept| +QType: <A:list[arb[int,str,bool]]> * <B:same[A]> + +QValue: +make: A = set[int[0,50]] +make: B = int[0,50] +expr: C = <A> * {<B>} +hint: <C> +test: <A> * {<B>} == <?> + +QValue: +make: A = set[int[0,50]] +make: B = int[0,50] +make: C = int[0,50] +expr: D = <A> * {<B>, <C>} +hint: <D> +test: <A> * {<B>, <C>} == <?> + +QValue: +prep: import Set; +make: A = set[int[0,50]] +make: B = same[A] +expr: C = size(<A> * <B>) +hint: <C> +test: size(<A> * <B>) == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/Union/Union.concept| +QChoice: When you compute the union of two sets, the number of elements in the result is always: +g: Smaller than or equal to the total number of elements in both sets. +b: Greater than or equal to the number of elements in both sets. +b: Greater than the number of elements in both sets. +b: Smaller than the number of elements in both sets. + +QType: <A:set[arb[int,str,real]]> + <B:same[A]> + +QValue: <A:set[arb[int,str,real]]> + <B:same[A]> + +QValue: +make: A = set[arb[int,str]] +make: B = same[A] +expr: C = <A> + <B> +hint: <B> +test: <A> + <?> == <C> + +QValue: <A:set[arb[0,int,str]]> + <B:same[A]> + +QValue: <A:set[arb[0,int,str]]> + {} +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/SubSet/SubSet.concept| +QType: <A:set[arb[0,int,str,real]]> <= <B:same[A]> + +QValue: +make: DIFF = set[int[0,100]] +make: A = same[DIFF] +expr: B = <A> + <DIFF> +expr: C = <A> <= <B> +hint: <C> +test: <A> <= <B> == <?> + +QValue: +make: A = set[arb[int,str,bool]] +expr: C = <A> <= <A> +hint: <C> +test: <A> <= <A> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/Insert/Insert.concept| +QChoice: When you insert an element in a set, the number of elements in the result is always: +g: Greater than or equal to the number of elements in the original set. +g: One larger than the number of elements in the original set. +b: Smaller than the number of elements in the original set. +b: One smaller than the number of elements in the original set. + +QType: +make: A = arb[int[0,100],str] +make: B = set[same[A]] +test: <A> + <B> + +QValue: +make: A = arb[int[0,100],str] +make: B = set[same[A]] +expr: H = <A> + <B> +hint: <H> +test: <A> + <B> == <?> + +QValue: +make: A = arb[int[0,100],str] +make: B = set[same[A]] +expr: H = <B> + <A> +hint: <H> +test: <B> + <A> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/Intersection/Intersection.concept| +QChoice: When you compute the intersection of two sets, the number of elements in the result is always: +b: Greater than the number of elements in both sets. +b: Greater than or equal to the number of elements in both sets. +b: Smaller than the number of elements in both sets. +g: Smaller than or equal to the number of elements in the smallest set. + + +QType: <A:list[int[0,100],str]> & <B:same[A]> + +QValue: +make: DIFF = set[int[0,100],str] +make: A = same[DIFF] +make: B = same[DIFF] +expr: A1 = <DIFF> + <A> +expr: B1 = <B> + <DIFF> +expr: C = <A1> & <B1> +hint: <C> +test: <A1> & <B1> == <?> + +QValue: +make: A = set[int[0,10],str] +make: B = same[A] +expr: C = <A> & <B> +hint: <C> +test: <A> & <B> == <?> +conceptFile: |file:///Users/paulklint/git/rascal/src/org/rascalmpl/courses/Rascal/Expressions/Values/Set/in/in.concept| +QType: <A:arb[int,str,bool]> in <B:list[same[A]]> + +QValue: +make: ELM = int[0,100] +make: A = set[same[ELM]] +expr: A1 = {<ELM>} + <A> +expr: C = <ELM> in <A1> +hint: <C> +test: <ELM> in <A1> == <?> + +QValue: +prep: import Set; +make: ELM = int[0,10] +make: A = set[same[ELM]] +expr: C = <ELM> in <A> +hint: <C> +test: <ELM> in <A> == <?> +value: true + +conceptFile: List.rsc: + +Questions: + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],4,6] +make: I = int[0,3] +expr: E = <L>[<I>] +expr: H = indexOf(<L>, <E>) +hint: indexOf +test: <?>(<L>, <E>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: E = arb[int,str] +make: L = list[same[E],4,6] +make: I = int[0,3] +expr: H = insertAt(<L>, <I>, <E>) +hint: insertAt +test: <?>(<L>, <I>, <E>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[int,0,5] +expr: H = intercalate(";", <L>) +hint: intercalate +test: <?>(";", <L>) == <H> + + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],0,5] +expr: H = index(<L>) +hint: index +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],1,6] +expr: H = head(<L>) +hint: head +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str], 1, 6] +expr: H = last(<L>) +hint: last +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str]] +expr: H = toSet(<L>) +hint: toSet +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],4,6] +make: N = int[0,3] +expr: H = take(<N>, <L>) +hint: take +test: <?>(<N>, <L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],4,6] +make: N = int[0,3] +expr: H = tail(<L>,<N>) +hint: tail +test: <?>(<L>,<N>) == <H> + + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],1,5] +expr: H = headTail(<L>) +hint: Use headTail. +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],4,6] +make: I = int[0,3] +make: J = int[0,3] +expr: L1 = [<L>[<I>], <L>[<J>], *<L>, <L>[<J>], <L>[<I>]] +expr: H = dup(<L1>) +hint: dup +test: <?>(<L1>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],2,5] +expr: H = getOneFrom(<L>) +hint: getOneFrom +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[bool,int,str],3,4] +make: N = int[0,2] +expr: H = drop(<N>, <L>) +hint: drop +test: <?>(<N>, <L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],0,5] +expr: H = reverse(<L>) +hint: reverse +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],3,4] +make: I = int[0,2] +expr: E = <L>[<I>] +expr: L1 = reverse(<L>) + <L> +expr: H = lastIndexOf(<L1>, <E>) +hint: lastIndexOf +test: <?>(<L1>, <E>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],0,5] +expr: H = index(<L>) +hint: index +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],1,5] +expr: H = pop(<L>) +hint: pop +test: <?>(<L>) == <H> + + +QValue: +prep: import List; +make: L = list[arb[int,str], 1, 6] +expr: H = max(<L>) +hint: max +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[int[-20,20]] +expr: H = takeWhile(<L>, bool(int x){ return x > 0;}) +hint: takeWhile +test: <?>(<L>, bool(int x){ return x > 0;}) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],3,4] +make: I = int[0,2] +expr: C = delete(<L>, <I>) +hint: delete +test: <?>(<L>, <I>) == <C> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[int,2,7] +expr: H = sum(<L>) +hint: sum +test: <?>(<L>) == <H> + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[int, 4, 4] +make: M = list[int, 4, 4] +expr: Z = zip(<L>,<M>) +hint: zip +test: <?>(<L>, <M>) == <Z> + + +QValue: +desc: Fill in the missing function name. +prep: import List; +make: L = list[arb[int,str],1,5] +expr: H = sort(<L>) +hint: sort +test: <?>(<L>) == <H> + + +QValue: +prep: import List; +make: L = list[int, 1,5] +expr: H = mapper(<L>, int(int n){ return n + 1; }) +hint: mapper +list: +int incr(int x) { return x + 1; } +test: <?>(<L>, incr) == <H> + +QValue: +desc: Complete this function that tests that a list of words forms a palindrome. A palindrome is a word that is symmetrical +and can be read +from left to right and from right to left. +list: +import List; +public bool isPalindrome(list[str] words){ + return words == <?>; +} +test: isPalindrome(["a", "b", "b", "a"]) == true; + + diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/ValueGenerator.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/ValueGenerator.rsc new file mode 100644 index 00000000000..44bac5b751b --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/ValueGenerator.rsc @@ -0,0 +1,333 @@ +module lang::rascal::tutor::questions::ValueGenerator + +import Boolean; +import String; +import util::Math; +import List; +import Set; +import String; +import DateTime; +import lang::rascal::tutor::questions::Questions; +import Type; + +lexical IntCon = [0-9]+ !>> [0-9]; +syntax Type = "arb" "[" IntCon depth "," {Type ","}+ elemTypes "]"; + +private list[Type] baseTypes = [(Type)`bool`, (Type)`int`, (Type)`real`, (Type)`num`, (Type)`str`, (Type)`loc`, (Type)`datetime`]; + +// Type generation + +Type generateType(Type tp){ + return + visit(tp){ + case (Type) `arb[<IntCon depth>,<{Type ","}+ elemTypes>]` => + generateArbType(toInt("<depth>"), elemTypes) + }; +} + +Type generateArbType(int n, {Type ","}+ prefs) = generateArbType(n, [p | p <- prefs]); + +Type generateArbType(int n, list[Type] prefs){ + //println("generateArbType: <n>, <prefs>"); + if(n <= 0){ + return getOneFrom(prefs); + } + + switch(arbInt(6)){ + case 0: { elemType = generateArbType(n-1, prefs); return (Type) `list[<Type elemType>]`; } + case 1: { elemType = generateArbType(n-1, prefs); return (Type) `set[<Type elemType>]`; } + case 2: { keyType = generateArbType(n-1, prefs); + valType = generateArbType(n-1, prefs); + return (Type) `map[<Type keyType>,<Type valType>]`; + } + case 3: return generateArbTupleType(n-1, prefs); + case 4: return generateArbRelType(n-1, prefs); + case 5: return generateArbLRelType(n-1, prefs); + } + return getOneFrom(prefs); +} + +int size({Type ","}+ ets) = size([et | et <- ets]); + +Type makeTupleType({Type ","}+ ets) = [Type] "tuple[<intercalate(",", [et | et <- ets])>]"; + +Type generateArbTupleType(int n, list[Type] prefs){ + arity = 1 + arbInt(5); + return [Type] "tuple[<intercalate(",", [generateArbType(n-1, prefs) | int _ <- [0 .. 1+arbInt(5)] ])>]"; +} + +Type generateArbRelType(int n, list[Type] prefs){ + return [Type] "rel[<intercalate(",", [generateArbType(n - 1, prefs) | int _ <- [0 .. 1+arbInt(5)] ])>]"; +} + +Type generateArbLRelType(int n, list[Type] prefs){ + return [Type] "lrel[<intercalate(",", [generateArbType(n - 1, prefs) | int _ <- [0 .. 1+arbInt(5)] ])>]"; +} + +// Value generation + +public str generateValue(Type tp){ + //println("generateValue(<tp>, <env>)"); + switch(tp){ + case (Type) `bool`: + return generateBool(); + + case (Type) `int`: + return generateInt(-100,100); + + case (Type) `int[<IntCon f>,<IntCon t>]`: + return generateInt(toInt("<f>"), toInt("<t>")); + + case (Type) `real`: + return generateReal(-100,100); + + case (Type) `real[<IntCon f>,<IntCon t>]`: + return generateReal(toInt("<f>"), toInt("<t>")); + + case (Type) `num`: + return generateNumber(-100,100); + + case (Type) `num[<IntCon f>,<IntCon t>]`: + return generateNumber(toInt("<f>"), toInt("<t>")); + + case (Type) `str`: + return generateString(); + + case (Type) `loc`: + return generateLoc(); + + case (Type) `datetime`: + return generateDateTime(); + + case (Type) `list[<Type et>]`: + return generateList(et); + + case (Type) `list[<Type et>][<IntCon f>,<IntCon t>]`: + return generateList(et, from=toInt("<f>"), to=toInt("<t>")); + + case (Type) `set[<Type et>]`: + return generateSet(et); + + case (Type) `set[<Type et>] [<IntCon f>,<IntCon t>]`: + return generateSet(et, from=toInt("<f>"), to=toInt("<t>")); + + case (Type) `map[<Type kt>,<Type vt>]`: + return generateMap(kt, vt); + + case (Type) `map[<Type kt>,<Type vt>][<IntCon f>,<IntCon t>]`: + return generateMap(kt, vt, from=toInt("<f>"), to=toInt("<t>")); + + case (Type) `tuple[<{Type ","}+ ets>]`: + return generateTuple(ets); + + case (Type) `rel[<{Type ","}+ ets>]`: + return generateSet(makeTupleType(ets)); + + case (Type) `lrel[<{Type ","}+ ets>]`: + return generateList(makeTupleType(ets)); + + case (Type) `value`: + return generateArb(0, baseTypes); + } + throw "Unknown type: <tp>"; +} + +set[set[str]] vocabularies = +{ +// Letters: +{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", +"Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}, + +// Sesame Street: +{"Bert", "Ernie", "Big Bird", "Cookie Monster", "Grover", "Elmo", +"Slimey the Worm", "Telly Monster", "Count Von Count", "Countess Darling Von Darling", +"Countess Von Backwards", "Guy Smiley", "Barkley the Dog", "Little Bird", +"Forgetful Jones", "Bruno", "Hoots The Owl", "Prince Charming", +"Mumford the Magician", "Two-Headed Monster"}, + +// Star trek + +// Dr Who + +// Star Wars: +{"Jar Jar Binks", "C-3PO", "E-3PO", "Boba Fett", "Isolder", "Jabba the Hut", +"Luke Skywalker", "Obi-Wan Kenobi", "Darth Sidious", "Pincess Leia", +"Emperor Palpatine", "R2-D2", "Admiral Sarn", "Boba Fett", +"Anakin Skywalker", "Sy Snootles", "Han Solo", "Tibor", "Darth Vader", +"Ailyn Vel", "Yoda", "Zekk", "Joh Yowza"}, + +// Game of Thrones: +{"Tyrion", "Cersei", "Daenerys", "Petyr", "Jorah", "Sansa", "Arya", + "Theon", "Bran", "Sandor", "Joffrey", "Catelyn", "Robb", "Ned", + "Viserys", "Khal", "Varys", "Samwell", "Bronn", "Tywin", "Shae", + "Jeor", "Gendry", "Tommen", "Jaqen", "Davos", "Melisandre", + "Margaery", "Stannis", "Ygritte", "Talisa", "Brienne", "Gilly", + "Roose", "Tormund", "Ramsay", "Daario", "Missandei", "Eilaria", + "The High Sparrow"}, + + // World of Warcraft: + + {"Archimonde", "Kil\'jaeden", "Mannoroth", "Ner\'zhul", "Sargeras", + "Balnazzar", "Magtheridon", "Mal\'Ganis", "Tichondrius", "Varimathras", + "Azgalor", "Hakkar", "Kazzak", "Detheroc", "Akama", "Velen", + "Alexstrasza", "Malygos", "Neltharion", "Nozdormu", "Ysera", + "Korialstrasz", "Kalecgos", "Brann", "Muradin"}, + +// Fruits: +{"Blackcurrant", "Redcurrant", "Gooseberry", "Eggplant", "Guava", +"Pomegranate", "Kiwifruit", "Grape", "Cranberry", "Blueberry", "Pumpkin", +"Melon", "Orange", "Lemon", "Lime", "Grapefruit", "Blackberry", + "Raspberry", "Pineapple", "Fig", "Apple", "Strawberry", + "Mandarin", "Coconut", "Date", "Prune", "Lychee", "Mango", "Papaya", + "Watermelon"}, + +// Herbs and spices: +{"Anise", "Basil", "Musterd", "Nutmeg", "Cardamon", "Cayenne", +"Celery", "Pepper", "Cinnamon", "Coriander", "Cumin", +"Curry", "Dill", "Garlic", "Ginger", "Jasmine", +"Lavender", "Mint", "Oregano", "Parsley", "Peppermint", +"Rosemary", "Saffron", "Sage", "Sesame", "Thyme", +"Vanilla", "Wasabi"} + +}; + +public str generateBool(){ + return toString(arbBool()); +} + + public str generateInt(int from, int to){ + return toString(from + arbInt(to - from)); +} + + public str generateReal(int from, int to){ + return toString(from + arbReal() * (to - from)); +} + +public str generateNumber(int from, int to){ + return (arbBool()) ? generateInt(from, to) : generateReal(from, to); +} + +public str generateLoc(){ + return "|file:///home/paulk/pico.trm|(0,1,\<2,3\>,\<4,5\>)"; + /* + scheme = getOneFrom(["http", "file", "stdlib", "cwd"]); + authority = "auth"; + host = "www.cwi.nl"; + port = "80"; + path = "/a/b"; + +uri: the URI of the location. Also subfields of the URI can be accessed: + +scheme: the scheme (or protocol) like http or file. Also supported is cwd: for current working directory (the directory from which Rascal was started). + +authority: the domain where the data are located. + +host: the host where the URI is hosted (part of authority). + +port: port on host (part ofauthority). + +path: path name of file on host. + +extension: file name extension. + +query: query data + +fragment: the fragment name following the path name and query data. + +user: user info (only present in schemes like mailto). + +offset: start of text area. + +length: length of text area + +begin.line, begin.column: begin line and column of text area. + +end.line, end.column end line and column of text area. +*/ +} + +public str generateDateTime(){ + year = 1990 + arbInt(150); + month = 1 + arbInt(11); + day = 1 + arbInt(6); + dt1 = createDate(year, month, day); + if(arbBool()) + return "<dt1>"; + hour = arbInt(24); + minute = arbInt(60); + second = arbInt(60); + milli = arbInt(1000); + dt2 = createTime(hour, minute, second, milli); + return "<joinDateAndTime(dt1, dt2)>"; +} + +public str generateString(){ + vocabulary = getOneFrom(vocabularies); + return "\"<getOneFrom(vocabulary)>\""; +} + +public str generateList(Type et, int from=0, int to=5){ + n = from; + if(from < to) + n = from + arbInt(to - from + 1); + elms = []; + while(size(elms) < n){ + elms += generateValue(et); + } + return "[<intercalate(", ", elms)>]"; +} + +public str generateSet(Type et, int from=0, int to=5){ + n = from; + if(from < to) + n = from + arbInt(to - from + 1); + elms = []; + attempt = 0; + while(size(elms) < n && attempt < 100){ + attempt += 1; + elm = generateValue(et); + if(elm notin elms) + elms += elm; + } + return "{<intercalate(", ", elms)>}"; +} + +public str generateMap(Type kt, Type vt, int from=0, int to=5){ + keys = { generateValue(kt) | int _ <- [from .. arbInt(to)] }; // ensures unique keys + keyList = toList(keys); + return "(<for(i <- index(keyList)){><(i==0)?"":", "><keyList[i]>: <generateValue(vt)><}>)"; +} + +public str generateTuple({Type ","}+ ets){ + return intercalate(", ", [generateValue(et) | et <-ets]); +} + +public str generateTuple(list[Type] ets){ + return intercalate(", ", [generateValue(et) | et <-ets]); +} + +public str generateRel({Type ","}+ ets){ + return "\<<for(int i <- [0 .. size(ets)]){><(i==0)?"":", "><generateValue(ets[i])><}>\>"; +} +public str generateRel(list[Type] ets){ + return "\<<for(int i <- [0 .. size(ets)]){><(i==0)?"":", "><generateValue(ets[i])><}>\>"; +} + +public str generateLRel({Type ","}+ ets){ + return "\<<for(int i <- [0 .. size(ets)]){><(i==0)?"":", "><generateValue(ets[i])><}>\>"; +} + +public str generateArb(int n, list[Type] prefs){ + if(n <= 0) + return generateValue(getOneFrom(prefs)); + + switch(arbInt(5)){ + case 0: return generateList(generateArbType(n-1, prefs)); + case 1: return generateSet(generateArbType(n-1, prefs)); + case 2: return generateMap(generateArbType(n-1, prefs), generateArbType(n-1, prefs)); + case 3: return generateTuple(prefs); + case 4: return generateRel(prefs); + } + + return generateValue(getOneFrom(prefs)); +} diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/tutor-prelude.js b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/tutor-prelude.js new file mode 100644 index 00000000000..03710e023a3 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/questions/tutor-prelude.js @@ -0,0 +1,434 @@ + +/* + * Prelude for Rascal Tutor + */ + +$('.hole').each(visitHole); +$('.code-question').each(visitCodeQuestion); +$('.choice-question').each(visitChoiceQuestion); +$('.click-question').each(visitClickQuestion); +$('.move-question').each(visitMoveQuestion); +$('.fact-question').each(visitFactQuestion); + +//JQuery plugin: +// See http://stackoverflow.com/questions/8100770/auto-scaling-inputtype-text-to-width-of-value +$.fn.textWidth = function(_text, _font){//get width of text with font. usage: $("div").textWidth(); + var fakeEl = $('<span>').hide().appendTo(document.body).text(_text || this.val() || this.text()).css('font', _font || this.css('font')), + width = fakeEl.width(); + fakeEl.remove(); + return width; + }; + +$.fn.autoresize = function(options){//resizes elements based on content size. usage: $('input').autoresize({padding:10,minWidth:0,maxWidth:100}); + options = $.extend({padding:10,minWidth:0,maxWidth:10000}, options||{}); + $(this).on('input', function() { + $(this).css({'width': Math.min(options.maxWidth,Math.max(options.minWidth,$(this).textWidth() + options.padding)), + 'border-radius': '5px'}); + }).trigger('input'); + return this; +} + +//have <input> resize automatically +$(".hole").autoresize({padding:20,minWidth:40,maxWidth:300}); + +// hole + +function visitHole(index){ + var id = $( this ).attr('id'); + var repl = $('<input type="text" class="hole" name="' + id + '">'); + $ (this).replaceWith(repl, $(this).children().html()); +} + +// click + +function handleClick(id){ + $("#" + id).attr('clicked', true); + return false; +} + +// CodeQuestion + +function submitCode(idGood, idBad, idFeedback){ + return "<input type='submit' value='Check It' style='clear:left;'>" + + "<img id ='" + idGood + "' height='25' width='25' src='/images/good.png' style='display:none;'/>" + + "<img id ='" + idBad + "' height='25' width='25' src='/images/bad.png' style='display:none;'/>" + + "<div id ='" + idFeedback + "'> </div>" + ; +} + + +function visitCodeQuestion(index){ + var id = $( this ).attr('id'); + var idGood = id + "-good"; + var idBad = id + "-bad" + var idForm = id + "-form"; + var idFeedback = id + "-feedback"; + var listing = $( this ).attr('listing'); + var content = $( this ).html(); + var $this = $( this ); + + $( this ).html("<form id='" + idForm + "' action='/ValidateCodeQuestion' method='POST'>" + + content + + "<input type='hidden' name='question' value='" + id + "'>" + + "<input type='hidden' name='listing' value='" + listing + "'>" + + submitCode(idGood, idBad, idFeedback) + + "</form><br>"); + $( this ).submit(function(event){ + event.preventDefault(); + $("#" + idBad).hide(100); + $("#" + idGood).hide(100); + $("#" + idFeedback).hide(100); + $("#" + idFeedback).html(""); + $.post("/ValidateCodeQuestion", + $( "#" + idForm ).serialize(), + function(jsonData,status,jqXHR){ + var msgs = ""; + var feedback = jsonData.feedback; + if(jsonData.ok == true){ + if(feedback != null){ + $("#" + idFeedback).html("<i>" + feedback + "</i>"); + $("#" + idFeedback).show(100); + } + $("#" + idGood).show(100); + } else { + $("#" + idBad).show(100); + var failed = jsonData.failed; + var exceptions = jsonData.exceptions; + var syntax = jsonData.syntax; + + if(syntax != null){ + if(syntax.beginLine == syntax.endLine){ + msgs = "Syntax error near line " + syntax.beginLine + ", column " + syntax.beginColumn; + } else { + msgs = "Syntax error at lines " + syntax.beginLine + "-" + syntax.endLine; + } + } else if(failed.length != 0){ + if(failed.length == 1){ + msgs = "Test failed (near line " + failed[0].src.beginLine + ")"; + if(failed[0].msg != ""){ + msgs += ": " + failed[0].msg; + } + msgs += "."; + } else { + msgs = "Tests failed: <ul>"; + for(var i = 0; i < failed.length; i++){ + msgs += "<li>Near line "+ failed[i].src.beginLine; + if(failed[i].msg != ""){ + msgs += ": " + failed[i].msg + "."; + } + msgs += "</li>" + } + msgs += "</ul>" + } + } else if(exceptions.length != 0){ + msgs = "exception occurred: "; + for(var i = 0; i < exceptions.length; i++){ + msgs += " " + exceptions[i]; + } + } + if(feedback != null){ + msgs += "\n<i>" + feedback + "</i>"; + } + $("#" + idFeedback).html("<p>" + msgs + "</p>"); + $("#" + idFeedback).show(100); + } + }, + "json"); + return false; + }); +} + +// ChoiceQuestion + +function visitChoiceQuestion(index){ + var id = $( this ).attr('id'); + var idGood = id + "-good"; + var idBad = id + "-bad" + var idForm = id + "-form"; + var idFeedback = id + "-feedback"; + var content = $( this ).html(); + $( this ).html("<form id='" + idForm + "'>" + + content + + submitCode(idGood, idBad, idFeedback) + + "</form><br>"); + $( this ).submit(function(event){ + validateChoiceQuestion(id,idGood,idBad,idFeedback) + return false; + }); +} + +function validateChoiceQuestion(id, idGood, idBad, idFeedback){ + event.preventDefault(); + $("#" + idBad).hide(100); + $("#" + idGood).hide(100); + $("#" + idFeedback).hide(100); + var checked = $('input[name=' + id + ']:checked'); + + if(checked.val() === "y"){ + $("#" + idGood).show(100); + } else { + $("#" + idBad).show(100); + } + var fb = checked.attr("feedback"); + $("#" + idFeedback).html("<p><i>" + fb + "</i></p>"); + $("#" + idFeedback).show(100); +} + +// ClickQuestion + +function visitClickQuestion(index){ + var id = $( this ).attr('id'); + var idGood = id + "-good"; + var idBad = id + "-bad" + var idForm = id + "-form"; + var idFeedback = id + "-feedback"; + var content = $( this ).html(); + $( this ).html("<form id='" + idForm + "'>" + + content + + submitCode(idGood, idBad, idFeedback) + + "</form><br>"); + $( this ).submit(function(event){ + validateClickQuestion(id,idGood,idBad,idFeedback) + return false; + }); +} + +function validateClickQuestion(id, idGood, idBad, idFeedback){ + event.preventDefault(); + $("#" + idBad).hide(100); + $("#" + idGood).hide(100); + $("#" + idFeedback).hide(100); + var missed = 0; + $("#" + id + " .clickable").each(function(index, object) { if($(object).attr('clicked') != "true") { missed++; }}); + + if(missed == 0){ + $("#" + idGood).show(100); + } else { + $("#" + idBad).show(100); + var msg = missed == 1 ? "You missed 1 click" : "You missed " + missed + " clicks."; + $("#" + idFeedback).html("<p><i>" + msg + "</i></p>"); + $("#" + idFeedback).show(100); + } +} + +// MoveQuestion + +var deltax = 20; +var deltay = 1; + +function visitMoveQuestion(index){ + var id = $( this ).attr('id'); + var idGood = id + "-good"; + var idBad = id + "-bad" + var idForm = id + "-form"; + var idFeedback = id + "-feedback"; + var content = $( this ).html(); + + $( this ).html("<form id='" + idForm + "' class='movable-code-form'>" + + content + + submitCode(idGood, idBad, idFeedback) + + "</form><br>"); + $( this ).submit(function(event){ + validateMoveQuestion(id,idGood,idBad,idFeedback) + return false; + }); + + // Custom grid + $("#" + id + " .movable-code").draggable({ + drag: function( event, ui ) { + var snapTolerance = $(this).draggable('option', 'snapTolerance'); + var topRemainder = ui.position.top % deltay; + var leftRemainder = ui.position.left % deltax; + + if (topRemainder <= snapTolerance) { + ui.position.top = ui.position.top - topRemainder; + } + + if (leftRemainder <= snapTolerance) { + ui.position.left = ui.position.left - leftRemainder; + } + }, + containment: "parent", + cursor: "pointer", + cursorAt: { top:0, left: 0 } + }); + + + + // $(".movable-code-form").submit(function(event){ + // validateMovedCode(event); + // return false; + // }); +} + +function inside(inner, outer){ + return inner.left > outer.left && + inner.top > outer.top && + inner.right < outer.right && + inner.bottom < outer.bottom; +} + +function above(upper, lower){ + return upper.bottom < lower.top; +} + +function near(y1, y2){ + return Math.abs(y1 - y2) < 3; +} + +function compare(r1, r2){ + return r1.top == r2.top ? 0 : (r1.bottom < r2.top) ? -1 : 1; +} + +function insideTarget(target, elem){ + var targetRect = target.getBoundingClientRect(); + var elemRect = elem.getBoundingClientRect(); + + return inside(elemRect, targetRect); +} + +function validateIndent(firstBox, box){ + var firstRect = firstBox.getBoundingClientRect(); + var boxRect = box.getBoundingClientRect(); + var indent = parseInt($(box).attr("indent")); + return boxRect.left - firstRect.left == indent * deltax; +} + +function validateMoveQuestion(id, idGood, idBad, idFeedback){ + event.preventDefault(); + $("#" + idBad).hide(100); + $("#" + idGood).hide(100); + $("#" + idFeedback).hide(100); + var mq = event.target.closest(".move-question"); + + var boxes = $(mq).find(".movable-code"); + boxes.each(function(index,box){ $(box).attr("placement", "none"); }); + + target = $(mq).find(".movable-code-target").get(0); + var insideboxes = boxes.filter(function(index, elem) { return insideTarget(target, elem); }); + + insideboxes = insideboxes.sort(function(x, y) { + return compare(x.getBoundingClientRect(), + y.getBoundingClientRect()); + }); + + var decoy = 0; + var wrong_placement = 0; + var wrong_indent = 0; + insideboxes.each(function(index, box){ + + var indexBox = parseInt($(box).attr("index")); + console.log("indexBox", indexBox, box); + + if(indexBox >= 0){ + var indentOK = index == 0 || validateIndent(insideboxes.get(0), box); + var indexOK = index == indexBox; + if(!indentOK){ + wrong_indent += 1; + } + if(!indexOK){ + wrong_placement += 1; + } + $(box).attr("placement", indexOK ? (indentOK ? "correct" : "wrong-indent") : "wrong"); + } else { + $(box).attr("placement", "wrong"); + decoy += 1; + } + }); + var missing = 0; + boxes.each(function(index, box){ + if($(box).attr("placement") === "none" && parseInt($(box).attr("index")) >= 0){ + missing += 1; + } + }); + if(decoy > 0 || wrong_placement > 0 || wrong_indent > 0 || missing > 0 ){ + $("#" + idBad).show(100); + var msg = ""; + if(decoy > 0){ + msg += incorrect_fragments(decoy, "decoy"); + } + if(missing){ + msg += incorrect_fragments(missing, "missing"); + } + if(wrong_placement > 0){ + msg += incorrect_fragments(wrong_placement, "incorrectly placed"); + } + if(wrong_indent > 0){ + msg += incorrect_fragments(wrong_indent, "incorrectly indented"); + } + + $("#" + idFeedback).html("<p><i>" + msg + "</i></p>"); + $("#" + idFeedback).show(100); + } else { + $("#" + idGood).show(100); + } +} + +function incorrect_fragments(n, msg){ + return n + " " + msg + " fragment" + (n == 1 ? "" : "s") + ". "; +} + +// ---- FactQuestion + +function visitFactQuestion(index){ + var id = $( this ).attr('id'); + var idGood = id + "-good"; + var idBad = id + "-bad" + var idForm = id + "-form"; + var idFeedback = id + "-feedback"; + var content = $( this ).html(); + var $this = $( this ); + + $( this ).html("<form id='" + idForm + "'>" + + content + + "<div style='clear:left;'/>" + + submitCode(idGood, idBad, idFeedback) + + "<br></form><br>"); + + $( ".sortableLeft" ).sortable({ + containment: "parent", + cursor: "pointer" + }).disableSelection(); + $( ".sortableRight" ).sortable({ + containment: "parent", + cursor: "pointer" + }).disableSelection(); + + $(this).find("li").addClass("ui-state-default"); + $( this ).submit(function(event){ + validateFactQuestion(id,idGood,idBad,idFeedback) + return false; + }); +} + +function validateFactQuestion(id, idGood, idBad, idFeedback){ + event.preventDefault(); + $("#" + idBad).hide(100); + $("#" + idGood).hide(100); + $("#" + idFeedback).hide(100); + var fq = event.target.closest(".fact-question"); + + var leftItems = $(fq).find(".sortableLeft li"); + var rightItems = $(fq).find(".sortableRight li"); + var wrong = 0; + if(leftItems.length == rightItems.length){ + leftItems.each(function(index, item){ + var indexLeft = parseInt($(item).attr("index")); + var indexRight = parseInt($(rightItems.get(index)).attr("index")); + if(indexLeft == indexRight){ + $(item).attr("placement", "correct"); + $(rightItems.get(index)).attr("placement", "correct"); + } else { + $(item).attr("placement", "wrong"); + $(rightItems.get(index)).attr("placement", "wrong"); + wrong += 1; + } + }); + } + if(wrong > 0){ + $("#" + idBad).show(100); + } else { + $("#" + idGood).show(100); + } + +} diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/ITutorScreenshotFeature.java b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/ITutorScreenshotFeature.java new file mode 100644 index 00000000000..02491cb229b --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/ITutorScreenshotFeature.java @@ -0,0 +1,32 @@ +package org.rascalmpl.tutor.lang.rascal.tutor.repl; + +import java.io.IOException; + +/** + * A interface to be implemented by a depending project. The + * screenshot feature is injected into the tutor command executor + * by dynamic loading this class: org.rascalmpl.tutor.Screenshotter + * + * The goal is to not have dependencies on large projects + * like selenium and Chrome in the core of the rascal project. + * + * The tutor will work fine without a screenshotter, except that + * screenshots will not be included with the documentation. It is + * advisable to run the tutor using the rascal-maven-plugin, which + * makes sure that the screenshot feature is properly injected. + */ +public interface ITutorScreenshotFeature { + + /** + * The URL string is expected to be syntactically correct. + * Typically it is localhost:<port>, to point at the right + * server that is currently visualizizing something from the + * REPL or the IDEServices. + * + * @param url localhost url + * @return a base64 encoded PNG snapshot. + * @throws IOExceptions when unexpected things happen + */ + String takeScreenshotAsBase64PNG(String url) throws IOException; + +} diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorCommandExecutor.java b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorCommandExecutor.java new file mode 100644 index 00000000000..a18893c7499 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorCommandExecutor.java @@ -0,0 +1,240 @@ +package org.rascalmpl.tutor.lang.rascal.tutor.repl; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.rascalmpl.debug.IRascalMonitor; +import org.rascalmpl.ideservices.IDEServices; +import org.rascalmpl.interpreter.Evaluator; +import org.rascalmpl.interpreter.utils.RascalManifest; +import org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages; +import org.rascalmpl.library.util.PathConfig; +import org.rascalmpl.parser.gtd.exception.ParseError; +import org.rascalmpl.repl.StopREPLException; +import org.rascalmpl.repl.output.IBinaryOutputPrinter; +import org.rascalmpl.repl.output.IErrorCommandOutput; +import org.rascalmpl.repl.output.IImageCommandOutput; +import org.rascalmpl.repl.output.IWebContentOutput; +import org.rascalmpl.repl.rascal.RascalInterpreterREPL; +import org.rascalmpl.shell.ShellEvaluatorFactory; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.uri.classloaders.SourceLocationClassLoader; +import org.rascalmpl.uri.project.ProjectURIResolver; +import org.rascalmpl.uri.project.TargetURIResolver; + +import io.usethesource.vallang.IList; +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValue; +import io.usethesource.vallang.io.StandardTextWriter; + +public class TutorCommandExecutor { + private final RascalInterpreterREPL interpreter; + private final StringWriter outWriter = new StringWriter(); + private final PrintWriter outPrinter = new PrintWriter(outWriter); + private final StringWriter errWriter = new StringWriter(); + private final PrintWriter errPrinter = new PrintWriter(errWriter, true); + private final ITutorScreenshotFeature screenshot; + + public TutorCommandExecutor(PathConfig pcfg) throws IOException, URISyntaxException{ + interpreter = new RascalInterpreterREPL() { + @Override + protected Evaluator buildEvaluator(Reader input, PrintWriter stdout, PrintWriter stderr, + IDEServices services) { + var eval = super.buildEvaluator(input, stdout, stderr, services); + eval.getConfiguration().setRascalJavaClassPathProperty(javaCompilerPathAsString(pcfg.getJavaCompilerPath())); + + ISourceLocation projectRoot = inferProjectRoot((ISourceLocation) pcfg.getSrcs().get(0)); + String projectName = new RascalManifest().getProjectName(projectRoot); + URIResolverRegistry reg = URIResolverRegistry.getInstance(); + reg.registerLogical(new ProjectURIResolver(projectRoot, projectName)); + reg.registerLogical(new TargetURIResolver(projectRoot, projectName)); + + for (IValue path : pcfg.getSrcs()) { + eval.addRascalSearchPath((ISourceLocation) path); + } + + for (IValue path : pcfg.getLibs()) { + eval.addRascalSearchPath((ISourceLocation) path); + } + + ClassLoader cl = new SourceLocationClassLoader(pcfg.getClassloaders(), ShellEvaluatorFactory.class.getClassLoader()); + eval.addClassLoader(cl); + + return eval; + } + + @Override + protected IDEServices buildIDEService(PrintWriter err, IRascalMonitor monitor, Terminal term) { + return (monitor instanceof IDEServices) ? (IDEServices)monitor : new TutorIDEServices(err); + } + + }; + + var terminal = TerminalBuilder.builder() + .system(false) + .streams(InputStream.nullInputStream(), OutputStream.nullOutputStream()) + .dumb(true) + .color(false) + .encoding(StandardCharsets.UTF_8) + .build(); + + + + interpreter.initialize(Reader.nullReader(), outPrinter, errPrinter, new TutorIDEServices(errPrinter), terminal); + screenshot = loadScreenShotter(); + } + + private ITutorScreenshotFeature loadScreenShotter() { + try { + return (ITutorScreenshotFeature) getClass() + .getClassLoader() + .loadClass("org.rascalmpl.tutor.Screenshotter") + .getDeclaredConstructor() + .newInstance(); + } + catch (ClassNotFoundException e) { + // that is normal; we just don't have the feature available. + return null; + } + catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { + throw new Error("WARNING: Could not load screenshot feature from org.rascalmpl.tutor.Screenshotter", e); + } + } + + private static ISourceLocation inferProjectRoot(ISourceLocation member) { + ISourceLocation current = member; + URIResolverRegistry reg = URIResolverRegistry.getInstance(); + while (current != null && reg.exists(current) && reg.isDirectory(current)) { + if (reg.exists(URIUtil.getChildLocation(current, "META-INF/RASCAL.MF"))) { + return current; + } + + if (URIUtil.getParentLocation(current).equals(current)) { + // we went all the way up to the root + return reg.isDirectory(member) ? member : URIUtil.getParentLocation(member); + } + + current = URIUtil.getParentLocation(current); + } + + return current; + } + + private String javaCompilerPathAsString(IList javaCompilerPath) { + StringBuilder b = new StringBuilder(); + + for (IValue elem : javaCompilerPath) { + ISourceLocation loc = (ISourceLocation) elem; + + if (b.length() != 0) { + b.append(File.pathSeparatorChar); + } + + // this is the precondition + assert loc.getScheme().equals("file"); + + // this is robustness in case of experimentation in pom.xml + if ("file".equals(loc.getScheme())) { + b.append(Paths.get(loc.getURI()).toAbsolutePath().toString()); + } + } + + return b.toString(); + } + + + public void reset() { + interpreter.cancelRunningCommandRequested(); + interpreter.cleanEnvironment(); + outPrinter.flush(); + outWriter.getBuffer().setLength(0); + errPrinter.flush(); + errWriter.getBuffer().setLength(0); + } + + public Map<String, String> eval(String line) throws InterruptedException, IOException { + Map<String, String> result = new HashMap<>(); + try { + var replResult = interpreter.handleInput(line); + if (replResult instanceof IErrorCommandOutput) { + ((IErrorCommandOutput)replResult).asPlain().write(errPrinter, true); + } + else if (replResult instanceof IImageCommandOutput) { + var img = ((IImageCommandOutput)replResult).asImage(); + result.put(img.mimeType(), uuencode(img)); + } + else if (replResult instanceof IWebContentOutput) { + var webResult = (IWebContentOutput)replResult; + try { + String pngImage = screenshot.takeScreenshotAsBase64PNG(webResult.webUri().toASCIIString()); + + if (!pngImage.isEmpty()) { + result.put("application/rascal+screenshot", pngImage); + } + } + catch (Throwable e) { + errPrinter.write(e.getMessage()); + } + } + // we ignore IAnsiCommandOutput, as we know that we cannot render that. + // else if (replResult instanceof IAnsiCommandOutput) {} + else if (replResult != null) { + var txt = new StringWriter(); + var txtPrinter = new PrintWriter(txt, false); + replResult.asPlain().write(txtPrinter, true); + txtPrinter.flush(); + result.put("text/plain", txt.toString()); + } + else { + result.put("text/plain", "ok\n"); + } + } catch (ParseError pe) { + ReadEvalPrintDialogMessages.parseErrorMessage(errPrinter, line, interpreter.promptRootLocation().getScheme(), pe, new StandardTextWriter(true)); + } + catch (StopREPLException e1) { + errWriter.write("Quiting REPL"); + } finally { + result.put("application/rascal+stdout", getPrintedOutput()); + result.put("application/rascal+stderr", getErrorOutput()); + } + return result; + } + + private String uuencode(IBinaryOutputPrinter content) throws IOException { + var result = new ByteArrayOutputStream(); + try (var wrapped = Base64.getEncoder().wrap(result)) { + content.write(wrapped); + } + return result.toString(StandardCharsets.ISO_8859_1); // help java recognize the compact strings can be used + } + + private String getPrintedOutput(){ + outPrinter.flush(); + String result = outWriter.toString(); + outWriter.getBuffer().setLength(0); + return result; + } + + private String getErrorOutput() { + errPrinter.flush(); + String result = errWriter.toString(); + errWriter.getBuffer().setLength(0); + return result; + } +} diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorCommandExecutor.rsc b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorCommandExecutor.rsc new file mode 100644 index 00000000000..63ff2e3e3de --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorCommandExecutor.rsc @@ -0,0 +1,57 @@ +module lang::rascal::tutor::repl::TutorCommandExecutor + +import util::Reflective; + +@synopsis{A closure-based object wrapper for Rascal REPL} +@description{ +Using an instance of CommandExecutor you can simulate the exact interactions +between a Rascal REPL user and the REPL. + +This was created to implement documentation pages with example REPL runs. +} +data CommandExecutor + = executor( + PathConfig pcfg, + void () reset, + map[str mimeType, str content] (str command) eval + ); + +@synopsis{Instantiates a ((CommandExecutor)) to simulate a REPL} +@examples{ +It's funny that the current example is also executed by a CommandExecutor of the tutor compiler. +Here we use to show how it works: + +```rascal-shell +import lang::rascal::tutor::repl::TutorCommandExecutor; +import util::Reflective; +e = createExecutor(pathConfig()); +// now we can find the current prompt: +e.prompt(); +// and evaluate an assignment +e.eval("x = 1;"); +// look what a continuation prompt looks like: +e.eval("println(\"abc\"") +e.prompt() +// finish the command we started +e.eval(")") +} +@javaClass{org.rascalmpl.tutor.lang.rascal.tutor.repl.TutorCommandExecutorCreator} +java CommandExecutor createExecutor(PathConfig pcfg); + +test bool executorSmokeTest() { + exec = createExecutor(pathConfig()); + + output = exec.eval("import IO;"); + + if (output["text/plain"] != "ok\n") { + return false; + } + + exec.eval("println(\"haai\")"); + + if (output["application/rascal+stdout"] != "haai\n") { + return false; + } + + return true; +} diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorCommandExecutorCreator.java b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorCommandExecutorCreator.java new file mode 100644 index 00000000000..40ca18286f5 --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorCommandExecutorCreator.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2022, Jurgen J. Vinju, Centrum Wiskunde & Informatica (NWO-I - CWI) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.tutor.lang.rascal.tutor.repl; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Map; + +import org.rascalmpl.exceptions.RuntimeExceptionFactory; +import org.rascalmpl.library.util.PathConfig; +import org.rascalmpl.values.IRascalValueFactory; +import org.rascalmpl.values.functions.IFunction; + +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IMapWriter; +import io.usethesource.vallang.IString; +import io.usethesource.vallang.type.Type; +import io.usethesource.vallang.type.TypeFactory; +import io.usethesource.vallang.type.TypeStore; + +/** + * This class marshalls between a virtual Rascal REPL and Rascal client code. + */ +public class TutorCommandExecutorCreator { + private final IRascalValueFactory vf; + private final Type resetType; + private final Type evalType; + private final Type execConstructor; + + public TutorCommandExecutorCreator(IRascalValueFactory vf, TypeFactory tf, TypeStore ts) { + this.vf = vf; + resetType = tf.functionType(tf.voidType(), tf.tupleEmpty(), tf.tupleEmpty()); + evalType = tf.functionType(tf.mapType(tf.stringType(), tf.stringType()), tf.tupleType(tf.stringType()), tf.tupleEmpty()); + execConstructor = ts.lookupConstructor(ts.lookupAbstractDataType("CommandExecutor"), "executor").iterator().next(); + } + + public IConstructor createExecutor(IConstructor pathConfigCons) { + try { + PathConfig pcfg = new PathConfig(pathConfigCons); + TutorCommandExecutor repl = new TutorCommandExecutor(pcfg); + return vf.constructor(execConstructor, + pathConfigCons, + reset(repl), + eval(repl) + ); + } + catch (IOException | URISyntaxException e) { + throw RuntimeExceptionFactory.io(vf.string(e.getMessage())); + } + } + + IFunction reset(TutorCommandExecutor exec) { + return vf.function(resetType, (args, kwargs) -> { + exec.reset(); + return null; + }); + } + + IFunction eval(TutorCommandExecutor exec) { + return vf.function(evalType, (args, kwargs) -> { + try { + IString command = (IString) args[0]; + Map<String, String> output = exec.eval(command.getValue()); + IMapWriter mw = vf.mapWriter(); + + for (String mimeType : output.keySet()) { + mw.put(vf.string(mimeType), vf.string(output.get(mimeType))); + } + + return mw.done(); + } + catch (InterruptedException | IOException e) { + throw RuntimeExceptionFactory.io(vf.string(e.getMessage())); + } + }); + } +} diff --git a/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorIDEServices.java b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorIDEServices.java new file mode 100644 index 00000000000..a1d271cc59a --- /dev/null +++ b/src/org/rascalmpl/tutor/lang/rascal/tutor/repl/TutorIDEServices.java @@ -0,0 +1,67 @@ +package org.rascalmpl.tutor.lang.rascal.tutor.repl; + +import java.io.PrintWriter; +import java.net.URI; + +import org.rascalmpl.ideservices.IDEServices; + +import io.usethesource.vallang.ISourceLocation; + +public class TutorIDEServices implements IDEServices { + + private final PrintWriter errorWriter; + + public TutorIDEServices(PrintWriter errorWriter) { + this.errorWriter = errorWriter; + } + + @Override + public void jobStart(String name, int workShare, int totalWork) { + + } + + @Override + public void jobStep(String name, String message, int workShare) { + + } + + @Override + public int jobEnd(String name, boolean succeeded) { + return 0; + } + + @Override + public boolean jobIsCanceled(String name) { + return false; + } + + @Override + public void jobTodo(String name, int work) { + + } + + @Override + public void warning(String message, ISourceLocation src) { + + } + + @Override + public PrintWriter stderr() { + return errorWriter; + } + + @Override + public void browse(URI uri, String title, int column) { + + } + + @Override + public void edit(ISourceLocation path) { + + } + + @Override + public void endAllJobs() { + + } +}