Download
561 lines 19.6 KiBNomsuModified 2019-04-07executable raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
#!/usr/bin/env nomsu -V7.0.0
###
    This is a Nomsu tutorial.
    
use "filesystem"
use "consolecolor"
use "commandline"
use "progressbar"
use "shell"
(lesson $name $lesson) compiles to ("
    {name=\($name as lua expr), lesson=\(
        quote ((SyntaxTree {.type = "FileChunks"} $lesson) as nomsu, text)
    )}
")

[<your code here>, ???] all compile to
    \(
        at \("Text" tree with "\((this tree).source)") fail
            "Incomplete code: This needs to be filled in."
    )

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

$lessons = [
    lesson "Variables":
        ### In Nomsu, variables have a "$" prefix, and you can just assign to them
            without declaring them first:
        $x = 1
        test that ($x == 1)
        
        ### Variables which have not yet been set have the value (nil)
        test that ($not_yet_set == (nil))
        
        ### Variables can be nameless:
        $ = 99
        
        ### Or have spaces, if surrounded with parentheses:
        $(my favorite number) = 23
        
        ### Figure out what value $my_var should have:
        $my_var = 100
        $my_var = ($my_var + $x + $(my favorite number))
        test that ($my_var == (???))
    
    lesson "Actions":
        ### In Nomsu, actions are bits of code that run to produce a value or
            do something. They have flexible syntax, so the values passed to
            them can be interspersed with the action's name in any order.
        ### Fix this action so the tests pass:
        ($x doubled) means ((???) * $x)
        
        ### Tests:
        test that ((2 doubled) == 4)
        test that ((-5 doubled) == -10)
    
    lesson "Blocks":
        ### When you need to do multiple things inside an action, use a block.
        ### Blocks are written with a colon followed by some indented code:
        ($x doubled then squared) means:
            $x = (2 * $x)
            $x = (???)
            return $x
        
        ### Blocks are also used for loops and conditionals:
        for $num in [0, -1, 10, 4]:
            $correct_answer = (4 * ($num * $num))
            if (($num doubled then squared) != $correct_answer):
                fail "Wrong answer for \($num)!"
    
    lesson "Text":
        ### Nomsu text is enclosed in double quotation marks:
        $text = "Hello"
        
        ### You can insert values into text using a backslash:
        test that ("two plus three is \(2 + 3)" == (???))
        
        ### Variables don't require parentheses, but other expressions do:
        $x = 99
        test that ("$x is \$x" == (???))
        
        ### This can be used to convert values to text:
        test that ("\$x" == (???))
        
        ### Longer strings use '("' followed by an indented region:
        $long = ("
            line one
              line two with spaces at the front
        ")
        
        test that
            $long == ("
                \(<your code here>)
                \(<your code here>)
            ")
    
    lesson "Conditionals":
        ### Make this action return "big" if its argument is bigger
            than 99, otherwise return "small"
        (the size of $n) means:
            if (???):
                <your code here>
            ..else:
                <your code here>
        
        ### Tests:
        for $big_number in [9999, 100]:
            test that ((the size of $big_number) == "big")
        
        for $small_number in [0, 1, -5, -999, 99]:
            test that ((the size of $small_number) == "small")
    
    lesson "Loops":
        ### Fix this action so the tests pass:
        (the sum of $numbers) means:
            $sum = 0
            
            ### You can loop over the values in a list like this:
            for $number in $numbers:
                ### Hint: math expressions may need parentheses
                <your code here>
            return $sum
        
        ### Tests:
        test that ((the sum of [1, 2, 3, 4, 5]) == 15)
        test that ((the sum of [100, 200]) == 300)
        
        ### You can also loop over a number range like this:
        $total = 0
        for $i in (1 to 3):
            $total = ($total + $i)
        test that ($total == (???))
    
    lesson "Variable Scopes":
        ### Nomsu's variables are local by default, and actions have their own scopes:
        $x = 1
        $y = 2
        
        ### Define an action that sets a variable:
        (do something) means:
            ### The variable $y is never set in this action, so it has the same value
                it has outside this action.
            test that ($y == (???))
            
            ### $x is set inside this action, and actions have their own scopes.
            $x = $y
            
            ### What number should $x be here?
            test that ($x == (???))
        
        ### After running the action, what value should $x have?
        do something
        test that ($x == (???))
    
    lesson "More Variable Scopes":
        ### Loops and conditionals do *not* have their own scopes:
        $z = 1
        if (1 == 1):
            ### Set $z inside a conditional:
            $z = 2
        
        ### After assigning in a conditional, what should $z be?
        test that ($z == (???))
        for $ in (1 to 1):
            ### Set $z inside a loop:
            $z = 3
        
        ### After assigning in a loop, what should $z be?
        test that ($z == (???))
    
    lesson "Externals":
        ### The 'external' block lets you modify variables outside an action:
        $x = 1
        (do something) means:
            external: $x = 2
        do something
        
        ### After running the action that sets $x in an 'external' block, what should $x be?
        test that ($x == (???))
    
    lesson "Locals":
        ### The 'with' block lets you create a local scope for the variables you list:
        $y = 1
        $z = 1
        with [$y]:
            $y = 2
            $z = 2
        
        ### After setting $y and $z in the 'with [$y]' block, what should $y and $z be?
        test that ($y == (???))
        test that ($z == (???))
    
    lesson "Failure and Recovery":
        $what_happened = "nothing"
        
        ### In Nomsu, sometimes things fail, but you can recover from failures with 'try':
        try:
            ### The 'fail' action triggers failure
            fail "Oh no!"
        ..if it fails:
            $what_happened = "failure"
        ..if it succeeds:
            $what_happened = "success"
        
        ### What do you think happened?
        test that ($what_happened == (???))
        
        ### Note: a 'try' block will silence failures, so this has no effect:
        try: fail
    
    lesson "Indexing":
        ### Nomsu uses the "." operator to access things inside an object:
        $dictionary = {.dog = "A lovable doofus", .cat = "An internet superstar"}
        test that ($dictionary.dog == "A lovable doofus")
        test that ($dictionary.cat == (???))
        
        ### If you try to access a key that's not in an object, the result is (nil):
        test that ($dictionary.mimsy == (???))
        
        ### $dictionary.dog is just a shorthand for $dictionary."dog".
            You may need to use the longer form for strings with spaces:
        $dictionary."guinea pig" = "A real-life tribble"
        
        ### Dictionaries are created with curly braces ({}) and can have
            anything as a key or value, including numbers or other dictionaries:
        $dictionary.5 = "The number five"
        $dictionary.five = 5
        $dictionary.myself = $dictionary
        test that ($dictionary.myself == (???))
        
        ### Lists are similar, but use square brackets ([])
            and can only have numbers as keys, starting at 1:
        $list = ["first", "second", 999]
        test that ($list.1 == "first")
        test that ($list.2 == (???))
        test that ($list.3 == (???))
        
        ### Hint: 4 should be a missing key
        test that ($list.4 == (???))
        test that ($list.foobar == (???))
        
        ### The "#" action gets the number of items inside something:
        test that (#$list == (???))
        test that (#{.x = 10, .y = 20} == (???))
    
    lesson "Methods":
        ### The "," is used for method calls, which means calling an action
            that's stored on an object (with the object as the first argument).
        ### Lists have an "add" method that puts new items at the end:
        $list = [-4, -6, 5]
        $list, add 3
        test that ($list == [-4, -6, 5, 3])
        $list, add 7
        test that ($list == [???])
        
        ### Text also has some methods like:
        $name = "Harry Tuttle"
        test that (($name, from 7 to 12) == "Tuttle")
        test that (($name, with "Tuttle" -> "Buttle") == (???))
        
        ### Methods can be chained too:
        test that (($name, with "Tuttle" -> "Buttle", from 7 to 12) == (???))
    
    lesson "Object Oriented Programming":
        ### Object Oriented Programming deals with things that have
            associated values and behaviors.
        ### Here, we define a Buffer to be a thing that has a .bits value:
        (a Buffer) is (a thing) with [$bits]:
            ### And some methods:
            ($self, set up) means:
                ### This method runs when a new buffer is created
                $bits = []
            
            ### This method is used to add to a buffer
            ($self, add $bit) means:
                $bits, add $bit
            
            ### ($list, joined) is a list method that concatenates the list items:
            ($self, as text) means ($bits, joined)
            
            ### Write a method called ($self, length) that returns the sum
                of the lengths of each bit in the buffer:
            <your code here>
        
        ### Create an instance of a Buffer:
        $b = (a Buffer)
        test that ($b is "a Buffer")
        test that ((type of $b) is "a Buffer")
        $b, add "xx"
        $b, add "yyy"
        test that (($b, as text) == "xxyyy")
        test that (($b, length) == 5)
        
        ### You can define a thing that inherits the behaviors of another thing like this:
        (a Backwards Buffer) is (a Buffer) with [$bits]:
            ### ($list, reversed) is a method that returns a copy of $list with
                the order of the items reversed.
            ($self, as text) means ($bits, reversed, joined)
        $bb = (a Backwards Buffer)
        $bb, add "one"
        $bb, add "two"
        test that (($bb, length) == (???))
        test that (($bb, as text) == (???))
    
    lesson "Files Part 1":
        ### Define an external action here:
        external:
            ### These will be used in the next lesson
            $global_var = 23
            ($x tripled) means:
                <your code here>
        
        test that ((5 tripled) == 15)
        test that ((2 tripled) == 6)
    
    lesson "Files Part 2":
        ### 'use' is the action for importing from other files.
        ### It takes the path to the file (without the .nom extension):
        use (<prev lesson>)
        test that ((10 tripled) == (???))
        test that ($global_var == (???))
]
$(ask normally) = $(ask)
command line program with $args:
    if ($args.help or $args.h):
        say ("
            Nomsu tutorial usage: nomsu -t tutorial [-x] [<location to put tutorial files>]
        ")
        exit
    
    say
        bold ("
            
                +------------------------------------+
                |   Welcome to the Nomsu tutorial!   |
                +------------------------------------+
            
        ")
    
    ### For this tutorial questions are hilighted in bold cyan:
    (ask $q) means (ask normally (bold (cyan $q)))
    
    ### Find the tutorial file directory:
    if ($args.extras is empty):
        $tutorial_dir = "./nomsu_tutorial"
        unless ($Files.exists $tutorial_dir):
            when
                ask "The Nomsu tutorial files will be in \$tutorial_dir is that okay? [Y/n/exit] "
            ..is:
                "n" "N" "no":
                    $tutorial_dir = (ask "Where do you want to put the tutorial? ")
                "exit" "quit" "q" "e": exit
    ..else:
        $tutorial_dir = $args.extras.1
    
    ### Set up the tutorial file directory:
    if (not ($Files.exists $tutorial_dir)):
        make directory $tutorial_dir
    (filename of $i) means ("\($tutorial_dir)/lesson\$i.nom", with "//" -> "/")
    for ($i = $lesson) in $lessons:
        $filename = (filename of $i)
        unless ($Files.exists $filename):
            $lesson_text =
                $lesson.lesson, with "%(<prev lesson>%)" ->
                    "\"\(filename of ($i - 1), with "%.nom$" -> "", with "^%./" -> "")\""
            
            write $lesson_text to file $filename
    
    ### Display info about editing the tutorial files:
    if $args.x:
        say ("
            The tutorial files are located in \$tutorial_dir.
            You're in charge of editing them yourself.
        ")
    ..else:
        $EDITOR = ($os.getenv "EDITOR")
        if $EDITOR:
            when
                ask ("
                    \$EDITOR is your system editor, would you like this tutorial to use it? [Y/n] \;
                ")
            ..is:
                "n" "N" "no":
                    $EDITOR = (nil)
        
        unless $EDITOR:
            say
            $EDITOR =
                ask ("
                    What program would you like to use to edit tutorial files?
                    (leave blank if you want to edit on your own in a different window)
                    > \;
                ")
            
            if ($EDITOR == ""):
                $EDITOR = (nil)
    
    say ("
        
        For this tutorial, a set of files has been created in \$tutorial_dir.
        Each file corresponds to a lesson. Your task is to fix each file
        so that it passes the tests.
        
    ")
    
    (run lesson $i) means:
        $filename = (filename of $i)
        $file = (read file $filename)
        $file = ($NomsuCode, from (Source $filename 1 #$file) $file)
        $tree = ($file parsed)
        $tree =
            $tree, with
                $ ->:
                    if ($ == \(<prev lesson>)):
                        return ("Text" tree with (filename of ($i - 1), with "%.nom$" -> ""))
        
        run $tree
    
    (get failures) means [:
        for ($i = $lesson) in $lessons:
            try:
                run lesson $i
            ..if it fails with $msg:
                $msg = ($msg, with "\n *stack traceback:.*" -> "")
                add {.lesson_number = $i, .failure = $msg}
    ]
    
    say (bold "Lessons:")
    (show first failure from $failures) means:
        say ("
            
            \(bold "Next thing to fix:") \(
                bold
                    yellow ("
                        Lesson \($failures.1.lesson_number): \($lessons.($failures.1.lesson_number).name)
                    ")
            )
            
            \($failures.1.failure, indented)
            
        ")
    $failures = (get failures)
    $current_lesson = (nil)
    for ($i = $lesson) in $lessons:
        for $f in $failures:
            if ($f.lesson_number == $i):
                $color = ((red) if $current_lesson else (yellow))
                $current_lesson or= $i
                say "\$color  - \(bold "\$color\$i. \($lesson.name) [incomplete]")"
                do next $lesson
        
        say "\(green "  + ")\(bold (green "\$i. \($lesson.name) [passed]"))"
    
    if $(COLOR ENABLED):
        say ("
            
            \(bold "Your progress:") \(20 wide ((#$lessons - #$failures) / #$lessons) progress bar)
        ")
    ..else:
        say
        say ((#$lessons - #$failures) / #$lessons progress bar)
    
    repeat until ($failures is empty):
        show first failure from $failures
        
        ### Have the user fix the first failure:
        unless $EDITOR:
            ### If the user is using an external editor, wait for the file to change
            $filename = (filename of $failures.1.lesson_number)
            say ("
                \(yellow "Waiting for you to fix ")\(bold $filename) \
                ..\(yellow "(press ctrl+c to exit)...")
            ")
            
            try:
                $files = [: for $ in (1 to #$lessons): add (read file (filename of $))]
                repeat:
                    sleep for 0.5 seconds
                    $new_files = [: for $ in (1 to #$lessons): add (read file (filename of $))]
                    if ($new_files != $files):
                        $files = $new_files
                        stop
            ..if it fails:
                say "\nGoodbye."
                exit
        ..else:
            ### If the user is using $EDITOR, launch it so they can edit the file:
            $filename = (filename of $failures.1.lesson_number)
            --- (retry file) ---
            when (ask "Edit \$filename to get it to pass? [Y/n/exit] ") is:
                "q" "quit" "exit" "n" "N" "no": exit
                "c":
                    write "# cheater!\n" to file $filename
                
                "y" "Y" "yes" "":
                    $f = (read file $filename)
                    [$line, $col] = ($failures.1.failure, match ":(%d+),(%d+)")
                    if ($line and $col):
                        when:
                            ($EDITOR, matches "vim$"):
                                sh> "\$EDITOR \$filename '+call cursor(\$line,\$col)'"
                            
                            ($EDITOR, matches "nano$"):
                                sh> "\$EDITOR +\$line,\$col \$filename"
                            
                            ($EDITOR, matches "emacs$"):
                                sh> "\$EDITOR +\$line:\$col \$filename"
                            
                            else:
                                sh> "\$EDITOR \$filename"
                    ..else:
                        sh> "\$EDITOR \$filename"
                
                else:
                    say "Sorry, I don't understand that."
                    go to (retry file)
            
            try:
                run lesson $failures.1.lesson_number
            ..if it fails with $msg:
                $failures.1.failure = $msg
                say (bold (red "\n There's a bit more to fix:"))
                $msg = ($msg, with "\n *stack traceback:.*" -> "")
                say ($msg, indented)
                say
                go to (retry file)
        
        $prev_progress = ((#$lessons - #$failures) / #$lessons)
        $failures = (get failures)
        $progress = ((#$lessons - #$failures) / #$lessons)
        
        ### Update the progressbar if progess has changed:
        if ($progress != $prev_progress):
            if ($progress > $prev_progress):
                say (bold (green "\nSuccess!\n"))
            ..else:
                say (bold (red "\nUh oh, that broke something.\n"))
            
            if $(COLOR ENABLED):
                $N = 100
                for $ in (0 to $N):
                    $k = (($ / $N) smoothed by 2)
                    $p = ($prev_progress to $progress mixed by $k)
                    say "\r\(bold "Your progress:") \(20 wide $p progress bar)" inline
                    $io.flush()
                    sleep for (1 / $N) seconds
            ..else:
                say ((#$lessons - #$failures) / #$lessons progress bar)
            
            say
    
    ### All done, no more failures:
    say ("
        
                    \(bold "\(slow blink "Congratulations!")")
        
               You've passed the tutorial!
        
                        \\(^\("ᴗ" if $(COLOR ENABLED) else "_")^)/
        
        
    ")