sketching out a slow tutorial

This commit is contained in:
Kartik K. Agaram 2021-10-20 11:51:24 -07:00
parent 909a0e2530
commit 619dc31dfc
15 changed files with 595 additions and 0 deletions

19
tutorial/add2.mu Normal file
View File

@ -0,0 +1,19 @@
fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
var result/eax: int <- do-add 3, 4
draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, result, 3/fg=cyan 0/bg
}
fn do-add a: int, b: int -> _/eax: int {
var result/eax: int <- copy a
result <- add b
return result
}
fn test-do-add {
var observed/eax: int <- do-add 0, 0
check-ints-equal observed, 0, "F - 0+0"
observed <- do-add 3, 0
check-ints-equal observed, 3, "F - 3+0"
observed <- do-add 3, 2
check-ints-equal observed, 5, "F - 3+2"
}

19
tutorial/c2f.mu Normal file
View File

@ -0,0 +1,19 @@
fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
var result/eax: int <- do-add 3, 4
draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, result, 3/fg=cyan 0/bg
}
fn do-add a: int, b: int -> _/eax: int {
var result/eax: int <- copy a
result <- add b
return result
}
fn test-do-add {
var observed/eax: int <- do-add 0, 0
check-ints-equal observed, 0, "F - 0+0"
observed <- do-add 3, 0
check-ints-equal observed, 3, "F - 3+0"
observed <- do-add 3, 2
check-ints-equal observed, 5, "F - 3+2"
}

138
tutorial/converter.mu Normal file
View File

@ -0,0 +1,138 @@
# Temperature Converter app
# https://eugenkiss.github.io/7guis/tasks/#temp
#
# To build:
# $ ./translate converter.mu
# To run:
# $ qemu-system-i386 code.img
# todo:
# less duplication
# error checking for input without hard-aborting
fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
# celsius numeric representation
var zero: float
var celsius/xmm1: float <- fahrenheit-to-celsius zero
# celsius string representation
var s-storage: (stream byte 0x10)
var s/ecx: (addr stream byte) <- address s-storage
write-float-decimal-approximate s, celsius, 2/decimal-places
# celsius input/display
var celsius-input-storage: gap-buffer
var celsius-input/esi: (addr gap-buffer) <- address celsius-input-storage
initialize-gap-buffer celsius-input, 8/capacity
load-gap-buffer-from-stream celsius-input, s
var cursor-in-celsius?/edx: boolean <- copy 0xffffffff/true
# fahrenheit numeric representation
var fahrenheit/xmm2: float <- celsius-to-fahrenheit celsius
# fahrenheit string representation
clear-stream s
write-float-decimal-approximate s, fahrenheit, 2/decimal-places
# fahrenheit input/display
var fahrenheit-input-storage: gap-buffer
var fahrenheit-input/edi: (addr gap-buffer) <- address fahrenheit-input-storage
initialize-gap-buffer fahrenheit-input, 8/capacity
load-gap-buffer-from-stream fahrenheit-input, s
var cursor-in-fahrenheit?/ebx: boolean <- copy 0/false # exactly one cursor boolean must be true at any time
# widget title
set-cursor-position screen, 0x1f/x 0xe/y
draw-text-rightward-from-cursor-over-full-screen screen, " Converter ", 0xf/fg 0x16/bg
# event loop
{
# draw current state to screen
clear-rect screen, 0x1f/xmin 0xf/ymin, 0x45/xmax 0x14/ymax, 0xc5/color
var x/eax: int <- render-gap-buffer screen, celsius-input, 0x20/x 0x10/y, cursor-in-celsius?, 7/fg 0/bg
x <- draw-text-rightward screen, " celsius = ", x, 0x45/xmax, 0x10/y, 7/fg 0xc5/bg
x <- render-gap-buffer screen, fahrenheit-input, x 0x10/y, cursor-in-fahrenheit?, 7/fg 0/bg
x <- draw-text-rightward screen, " fahrenheit", x, 0x45/xmax, 0x10/y, 7/fg 0xc5/bg
# render a menu bar
set-cursor-position screen, 0x21/x 0x12/y
draw-text-rightward-from-cursor-over-full-screen screen, " tab ", 0/fg 0x5c/bg=highlight
draw-text-rightward-from-cursor-over-full-screen screen, " switch sides ", 7/fg 0xc5/bg
draw-text-rightward-from-cursor-over-full-screen screen, " enter ", 0/fg 0x5c/bg=highlight
draw-text-rightward-from-cursor-over-full-screen screen, " convert ", 7/fg 0xc5/bg
# process a single keystroke
$main:input: {
var key/eax: byte <- read-key keyboard
var key/eax: grapheme <- copy key
compare key, 0
loop-if-=
# tab = switch cursor between input areas
compare key, 9/tab
{
break-if-!=
cursor-in-celsius? <- not
cursor-in-fahrenheit? <- not
break $main:input
}
# enter = convert in appropriate direction
compare key, 0xa/newline
{
break-if-!=
{
compare cursor-in-celsius?, 0/false
break-if-=
clear-stream s
emit-gap-buffer celsius-input, s
celsius <- parse-float-decimal s
fahrenheit <- celsius-to-fahrenheit celsius
clear-stream s
write-float-decimal-approximate s, fahrenheit, 2/decimal-places
clear-gap-buffer fahrenheit-input
load-gap-buffer-from-stream fahrenheit-input, s
}
{
compare cursor-in-fahrenheit?, 0/false
break-if-=
clear-stream s
emit-gap-buffer fahrenheit-input, s
{
var tmp/xmm1: float <- parse-float-decimal s
fahrenheit <- copy tmp
}
celsius <- fahrenheit-to-celsius fahrenheit
clear-stream s
write-float-decimal-approximate s, celsius, 2/decimal-places
clear-gap-buffer celsius-input
load-gap-buffer-from-stream celsius-input, s
}
break $main:input
}
# otherwise pass key to appropriate input area
compare cursor-in-celsius?, 0/false
{
break-if-=
edit-gap-buffer celsius-input, key
break $main:input
}
compare cursor-in-fahrenheit?, 0/false
{
break-if-=
edit-gap-buffer fahrenheit-input, key
break $main:input
}
}
loop
}
}
fn fahrenheit-to-celsius f: float -> _/xmm1: float {
var result/xmm1: float <- copy f
var thirty-two/eax: int <- copy 0x20
var thirty-two-f/xmm0: float <- convert thirty-two
result <- subtract thirty-two-f
var factor/xmm0: float <- rational 5, 9
result <- multiply factor
return result
}
fn celsius-to-fahrenheit c: float -> _/xmm2: float {
var result/xmm1: float <- copy c
var factor/xmm0: float <- rational 9, 5
result <- multiply factor
var thirty-two/eax: int <- copy 0x20
var thirty-two-f/xmm0: float <- convert thirty-two
result <- add thirty-two-f
return result
}

161
tutorial/converter2.mu Normal file
View File

@ -0,0 +1,161 @@
# Temperature Converter app
# https://eugenkiss.github.io/7guis/tasks/#temp
#
# To build:
# $ ./translate converter2.mu
# To run:
# $ qemu-system-i386 code.img
# todo:
# error checking for input without hard-aborting
fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
# imgui approach
forever {
number-input fahrenheit, cursor-in-fahrenheit?
number-input celsius, cursor-in-celsius?
if (menu-key 9/tab "Tab" "switch sides") { # requires non-blocking input
cursor-in-celsius? <- not
cursor-in-fahrenheit? <- not
}
if (menu-key 0xa/newline "Enter" "convert") {
if cursor-in-fahrenheit
celsius = fahrenheit-to-celsius fahrenheit
else
fahrenheit = celsius-to-fahrenheit celsius
}
}
# celsius numeric representation
var zero: float
var celsius/xmm1: float <- fahrenheit-to-celsius zero
# celsius input/display
var celsius-input-storage: gap-buffer
var celsius-input/esi: (addr gap-buffer) <- address celsius-input-storage
initialize-gap-buffer celsius-input, 8/capacity
value-to-input celsius, celsius-input
# fahrenheit numeric representation
var fahrenheit/xmm2: float <- celsius-to-fahrenheit celsius
# fahrenheit input/display
var fahrenheit-input-storage: gap-buffer
var fahrenheit-input/edi: (addr gap-buffer) <- address fahrenheit-input-storage
initialize-gap-buffer fahrenheit-input, 8/capacity
value-to-input fahrenheit, fahrenheit-input
# cursor toggle
var cursor-in-celsius?/edx: boolean <- copy 0xffffffff/true
#
render-title screen
# event loop
{
# render
render-state celsius-input, fahrenheit-input, cursor-in-celsius?
render-menu-bar screen
# process a single keystroke
$main:input: {
var key/eax: byte <- read-key keyboard
var key/eax: grapheme <- copy key
compare key, 0
loop-if-=
# tab = switch cursor between input areas
compare key, 9/tab
{
break-if-!=
cursor-in-celsius? <- not
break $main:input
}
# enter = convert in appropriate direction
compare key, 0xa/newline
{
break-if-!=
{
compare cursor-in-celsius?, 0/false
break-if-=
var tmp/xmm0: float <- input-to-value celsius-input
celsius <- copy tmp
fahrenheit <- celsius-to-fahrenheit celsius
value-to-input fahrenheit, fahrenheit-input
break $main:input
}
var tmp/xmm0: float <- input-to-value fahrenheit-input
fahrenheit <- copy tmp
celsius <- fahrenheit-to-celsius fahrenheit
value-to-input celsius, celsius-input
break $main:input
}
# otherwise pass key to appropriate input area
compare cursor-in-celsius?, 0/false
{
break-if-=
edit-gap-buffer celsius-input, key
break $main:input
}
edit-gap-buffer fahrenheit-input, key
}
loop
}
}
# business logic
fn fahrenheit-to-celsius f: float -> _/xmm1: float {
var result/xmm1: float <- copy f
var thirty-two/eax: int <- copy 0x20
var thirty-two-f/xmm0: float <- convert thirty-two
result <- subtract thirty-two-f
var factor/xmm0: float <- rational 5, 9
result <- multiply factor
return result
}
fn celsius-to-fahrenheit c: float -> _/xmm2: float {
var result/xmm1: float <- copy c
var factor/xmm0: float <- rational 9, 5
result <- multiply factor
var thirty-two/eax: int <- copy 0x20
var thirty-two-f/xmm0: float <- convert thirty-two
result <- add thirty-two-f
return result
}
# helpers for UI
fn input-to-value in: (addr gap-buffer) -> _/xmm0: float {
var s-storage: (stream byte 0x10)
var s/ecx: (addr stream byte) <- address s-storage
emit-gap-buffer in, s
var result/xmm1: float <- parse-float-decimal s
return result
}
fn value-to-input in: float, out: (addr gap-buffer) {
var s-storage: (stream byte 0x10)
var s/ecx: (addr stream byte) <- address s-storage
write-float-decimal-approximate s, in, 2/decimal-places
clear-gap-buffer out
load-gap-buffer-from-stream out, s
}
# helpers for rendering to screen
# magic constants here need to be consistent between functions
fn render-title screen: (addr screen) {
set-cursor-position screen, 0x1f/x 0xe/y
draw-text-rightward-from-cursor-over-full-screen screen, " Converter ", 0xf/fg 0x16/bg
}
fn render-state screen: (addr screen), c: (addr gap-buffer), f: (addr gap-buffer), cursor-in-c?: boolean {
clear-rect screen, 0x1f/xmin 0xf/ymin, 0x45/xmax 0x14/ymax, 0xc5/color
var x/eax: int <- render-gap-buffer screen, c, 0x20/x 0x10/y, cursor-in-c?, 7/fg 0/bg
x <- draw-text-rightward screen, " celsius = ", x, 0x45/xmax, 0x10/y, 7/fg 0xc5/bg
var cursor-in-f?/ecx: boolean <- copy cursor-in-c?
cursor-in-f? <- not
x <- render-gap-buffer screen, f, x 0x10/y, cursor-in-f?, 7/fg 0/bg
x <- draw-text-rightward screen, " fahrenheit", x, 0x45/xmax, 0x10/y, 7/fg 0xc5/bg
}
fn render-menu-bar screen: (addr screen) {
set-cursor-position screen, 0x21/x 0x12/y
draw-text-rightward-from-cursor-over-full-screen screen, " tab ", 0/fg 0x5c/bg=highlight
draw-text-rightward-from-cursor-over-full-screen screen, " switch sides ", 7/fg 0xc5/bg
draw-text-rightward-from-cursor-over-full-screen screen, " enter ", 0/fg 0x5c/bg=highlight
draw-text-rightward-from-cursor-over-full-screen screen, " convert ", 7/fg 0xc5/bg
}

35
tutorial/counter.mu Normal file
View File

@ -0,0 +1,35 @@
# Counter app
# https://eugenkiss.github.io/7guis/tasks/#counter
#
# To build:
# $ ./translate counter.mu
# To run:
# $ qemu-system-i386 code.img
fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk) {
var count/ecx: int <- copy 0
# widget title
set-cursor-position screen, 0x1f/x 0xe/y
draw-text-rightward-from-cursor-over-full-screen screen, " Counter ", 0xf/fg 0x16/bg
# event loop
{
# draw current state to screen
clear-rect screen, 0x1f/xmin 0xf/ymin, 0x40/xmax 0x14/ymax, 0xc5/color
set-cursor-position screen, 0x20/x 0x10/y
draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, count, 7/fg 0xc5/bg
# render a menu bar
set-cursor-position screen, 0x24/x 0x12/y
draw-text-rightward-from-cursor-over-full-screen screen, " enter ", 0/fg 0x5c/bg=highlight
draw-text-rightward-from-cursor-over-full-screen screen, " increment ", 7/fg 0xc5/bg
# process a single keystroke
{
var key/eax: byte <- read-key keyboard
compare key, 0
loop-if-=
compare key, 0xa/newline
break-if-!=
count <- increment
}
loop
}
}

169
tutorial/index.md Normal file
View File

@ -0,0 +1,169 @@
# A slow tour through Mu software on x86 computers
[Mu](https://github.com/akkartik/mu) shrinks all the software in a computer
until it can (in principle) fit in a single head. Sensible error messages with
as little code as possible, starting all the way from your (x86) processor's
instruction set. Everything easy to change to your needs
([habitable](http://akkartik.name/post/habitability)), everything easy to
check up on ([auditable](http://akkartik.name/post/neighborhood)).
This page is a guided tour through Mu's Readme and reference documentation.
We'll start out really slow and gradually accelerate as we build up skills. By
the end of it all, I hope you'll be able to program your processor to run some
small graphical programs. The programs will only use a small subset of your
computer's capabilities; there's still a lot I don't know and therefore cannot
teach. However, the programs will run on a _real_ processor without needing
any other intermediary software.
_Prerequisites_
You will need:
* A computer with an x86 processor running Linux. We're going to slowly escape
Linux, but we'll need it at the start. Mu works on other platforms, but be
warned that things will be _much_ (~20x) slower.
* Some fluency in typing commands at the terminal and interpreting their
output.
* Fluency with some text editor. Things like undo, copying and pasting text,
and saving work in files. A little experience programming in _some_ language
is also handy.
* [Git](https://git-scm.com) for version control.
* [QEMU](https://www.qemu.org) for emulating a processor without Linux.
* Basic knowledge of number bases, and the difference between decimal and
hexadecimal numbers.
If you have trouble with any of this, [I'm always nearby and available to
answer questions](http://akkartik.name/contact). The prerequisites are just
things I haven't figured out how to explain yet. In particular, I want this
page to be accessible to people who are in the process of learning
programming, but I'm sure it isn't good enough yet for that. Ask me questions
and help me improve it.
# Task 1: getting started
Open a terminal and run the following commands to prepare Mu on your computer:
```
git clone https://github.com/akkartik/mu
cd mu
```
Run a small program to start:
```
./translate apps/ex5.mu
qemu-system-i386 code.img
```
If you aren't on Linux, the command for creating `code.img` will be slightly
different:
```
./translate_emulated apps/ex5.mu
qemu-system-i386 code.img
```
Either way, you should see this:
<img alt='screenshot of hello world on the Mu computer' src='task1.png'>
If you have any trouble at this point, don't waste _any_ time thinking about
it. Just [get in touch](http://akkartik.name/contact).
(You can look at `apps/ex5.mu` at this point if you like. It's just a few
lines long. But don't worry if it doesn't make much sense.)
# Task 2: running tests
Here's a new program to run:
```
./translate tutorial/task2.mu
qemu-system-i386 code.img
```
(As before, I'll leave you to substitute `translate` with `translate_emulated`
if you're not on Linux.)
This time the screen will look like this:
<img alt='screenshot of failing test on the Mu computer' src='task2.png'>
Each of the dots is a _test_, a little self-contained and automated program
run with an expected result. Mu comes with a lot of tests, and it always runs
all tests before it runs any program. You may have missed the dots when you
ran Task 1 because there were no failures. They were printed on the screen and
then immediately erased. In Task 2, however, we've deliberately included a
failing test. When any tests fail, Mu will immediately stop, showing you
messages from failing tests and implicitly asking you to first fix them.
(Don't worry just yet about what the message in the middle of all the dots means.)
# Task 3: configure your text editor
So far we haven't used a text editor yet, but we will now be starting to do
so. Before we do, it's worth spending a little bit of time setting your
preferred editor up to be a little more ergonomic. Mu comes with _syntax
highlighting_ settings for a few common text editors in the `editor/`
sub-directory. If you don't see your text editor there, or if you don't know
what to do with those files, [get in touch!](http://akkartik.name/contact)
Here's what my editor (Vim) looks like with these settings on the program of
Task 1:
<img alt='Vim text editor rendering some colors in a Mu program' src='task3.png'>
It's particularly useful to highlight _comments_ which the computer ignores
(everything on a line after a `#` character) and _strings_ within `""` double
quotes.
# Task 4: your first Mu statement
Mu is a statement-oriented language. Read the first section of the [Mu syntax
description](https://github.com/akkartik/mu/blob/main/mu.md) to learn a little
bit about it.
Here's a skeleton of a Mu function that's missing a single statement.
```
fn the-answer -> _/eax: int {
var result/eax: int <- copy 0
# insert your statement below {
# }
return result
}
```
Try running it now:
```
./translate tutorial/task4.mu
qemu-system-i386 code.img
```
(As before, I'll leave you to substitute `translate` with `translate_emulated`
if you're not on Linux.)
You should see a failing test that looks something like this:
<img alt='screenshot of the initial (failing) state of task 4' src='task4-initial.png'>
Open `tutorial/task4.mu` in your text editor. Think about how to add a line
between the `{}` lines to make `the-answer` return 42. Rerun the above
commands. You'll know you got it right all the tests pass, i.e. when the rows
of dots and text above are replaced by an empty screen.
Don't be afraid to run the above commands over and over again as you try out
different solutions. Here's a way to run them together so they're easy to
repeat.
```
./translate tutorial/task4.mu && qemu-system-i386 code.img
```
In programming there is no penalty for making mistakes, and once you arrive at
the correct solution you have it forever. As always, [feel free to ping me and
ask questions or share your experience](http://akkartik.name/contact).
One gotcha to keep in mind is that numbers in Mu must always be in hexadecimal
notation, starting with `0x`. Use a calculator on your computer or phone to
convert 42 to hexadecimal, or [this page on your web browser](http://akkartik.github.io/mu/tutorial/task4-calculator.html).

BIN
tutorial/task1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

7
tutorial/task2.mu Normal file
View File

@ -0,0 +1,7 @@
fn test-1 {
check-ints-equal 1, 2, "F - test-1"
}
fn main {
# do nothing
}

BIN
tutorial/task2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
tutorial/task3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1,15 @@
<script>
function convert() {
var n = parseInt(document.getElementById('input').value);
if (isNaN(n))
document.getElementById('result').innerHTML = "not a number";
else
document.getElementById('result').innerHTML = "0x" + n.toString(16);
event.preventDefault();
}
</script>
<body style='font-size:200%'>
<form style='margin:5em' onSubmit='convert()'>
<input style='width:4em; font-size:100%' type='text' id='input'/> <input type='submit' style='width=6em; font-size:100%' value='convert'> = <span id='result' style='display:inline-block; min-width:4em; border-bottom:1px solid; text-align: right'></span>
</form>
</body>

BIN
tutorial/task4-initial.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,15 @@
fn the-answer -> _/eax: int {
var result/eax: int <- copy 0
# insert your statement below {
result <- copy 0x2a
# }
return result
}
fn test-the-answer {
var result/eax: int <- the-answer
check-ints-equal result, 0x2a, "F - the-answer should return 42, but didn't."
}
fn main {
}

15
tutorial/task4.mu Normal file
View File

@ -0,0 +1,15 @@
fn the-answer -> _/eax: int {
var result/eax: int <- copy 0
# insert your statement below {
# }
return result
}
fn test-the-answer {
var result/eax: int <- the-answer
check-ints-equal result, 0x2a, "F - the-answer should return 42, but didn't."
}
fn main {
}

2
tutorial/vimrc.vim Normal file
View File

@ -0,0 +1,2 @@
" when opening files in this directory, load vimrc from cwd (top-level)
source vimrc.vim