vim-literate-markdown

A Vim plugin to replicate a subset of Org mode's literate programming, for Markdown files.
git clone git://git.alex.balgavy.eu/vim-literate-markdown.git
Log | Files | Refs | README

commit 77a4d89ae20dd147c482ef4e0c92c26305f293b8
parent 8f9486a263b5c74cfd15935a3fbb43bc729a0b22
Author: Alex Balgavy <alex@balgavy.eu>
Date:   Thu,  1 Jul 2021 15:14:36 +0200

Added macro support & example document

Macros mean you can essentially describe your program in any order!

Diffstat:
MREADME.md | 7+++++--
Mautoload/literate_markdown.vim | 422+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mdoc/literate-markdown.txt | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Aexamples/myfact.md | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 525 insertions(+), 102 deletions(-)

diff --git a/README.md b/README.md @@ -9,14 +9,17 @@ So for example, tangling the code from the current file, or executing blocks of For this purpose, it provides some mappings and commands for Markdown files. This plugin is a work in progress and far from complete. -For more information about its features, see doc/literate-markdown.txt. +For more information about its features, see [`doc/literate-markdown.txt`](doc/literate-markdown.txt) (best viewed in Vim). +For examples of literate programming, see [`examples/`](examples/). **Disclaimer:** this code is not extensively tested. It works for me, running Vim 8.2 in the terminal on macOS, but it's not guaranteed to work for others. It might not work in Neovim, as I don't use Neovim. ## Why? Literate programming is a great way to provide explanation about what you're doing in code. -Emacs does this very well; I didn't know about anything similar for Vim, particularly for Markdown files. +It also lets you document what you're doing without the need to follow the exact order of the code, unlike comments embedded in source code. +You can e.g. layout the overall structure of a program and then jump immediately to some specific computation. +Emacs handles literate programming quite well in Org mode; I didn't know about anything similar for Vim, particularly for Markdown files. So I wrote this to allow literate programming in Markdown, because in my opinion Markdown is a much more universal syntax than Org. ## Installation diff --git a/autoload/literate_markdown.vim b/autoload/literate_markdown.vim @@ -3,8 +3,6 @@ if exists('g:loaded_literate_markdown_autoload') endif let g:loaded_literate_markdown_autoload = 1 -" TODO: use exceptions, refactor code - let s:codeblock_start = '^ *```[a-z]\+' let s:codeblock_end = '^ *```$' let s:tangle_directive = '<!-- *:Tangle' @@ -12,18 +10,24 @@ let s:result_comment_start = '<!--\nRESULT:' let s:result_comment_end = '^-->' let s:ALL_INTERP = '' -" returns [interpreter, filename] function! s:ParseTangleDirective(ln) - let theline = getline(a:ln)->matchlist(':Tangle\(([a-zA-Z0-9]*)\)* \(.*\) -->')[0:2] + let theline = getline(a:ln)->matchlist('\v:Tangle(\([a-zA-Z0-9]*\))* (\<\^?\>)? ?(\<[^>]+\>\+?)? ?(.*)? --\>')[1:4] if empty(theline) - echoerr 'No filename in Tangle directive on line ' .. a:ln - return [] + throw 'Cannot parse tangle directive on line ' .. a:ln + endif + let [interp, should_expand_macros, macro_group, fname] = theline + + if empty(should_expand_macros) && empty(macro_group) && empty(fname) + throw 'No filename in tangle directive on line ' .. a:ln endif - let [_, interp, fname] = theline - if fname[0] !=# '/' - let fname = expand("%:p:h") . '/' . fname + + if !empty(fname) + if fname[0] !=# '/' + let fname = expand("%:p:h") . '/' . fname + endif + let fname = fnameescape(fname) endif - return [s:Lang2Interpreter(interp[1:-2]), fnameescape(fname)] + return [s:Lang2Interpreter(interp[1:-2]), should_expand_macros, macro_group, fnameescape(fname)] endfunction " returns end line of a code block @@ -52,10 +56,72 @@ function! s:GetBlockContents(start, end) return retval endfunction +function! s:AddBlock(interps_files, block, block_start_line, last_set_interp, curfiles) + let interps_files = a:interps_files + + if type(a:block) ==# v:t_dict + let block_contents = a:block['contents'] + else + let block_contents = a:block + endif + + " Find out the amount of leading indentation (using the first line) + " TODO: this should be the least indented line + let nleadingspaces = matchend(block_contents[0], '^ \+') + if nleadingspaces ==# -1 + let nleadingspaces = 0 + endif + + " Get the interpreter for this block + let block_interp = s:GetBlockInterpreter(a:block_start_line) + if !empty(block_interp) + " Allow overriding all interpreters to a 'general' file: + " If the last Tangle directive didn't have an interpreter, direct + " all blocks to that file + if a:last_set_interp ==# s:ALL_INTERP && has_key(a:curfiles, s:ALL_INTERP) + " Get the current file for 'all interpreters' + let curfile = a:curfiles[s:ALL_INTERP] + let curinterp = s:ALL_INTERP + " If the last Tangle directive specified an interpreter + else + " If the interpreter was specified in a Tangle directive, use its + " current file + if has_key(interps_files, block_interp) + let curfile = a:curfiles[block_interp] + let curinterp = block_interp + " Otherwise, use the 'general' file if specified + elseif has_key(interps_files, s:ALL_INTERP) + let curfile = a:curfiles[s:ALL_INTERP] + let curinterp = s:ALL_INTERP + endif + endif + + " Add the lines to the current file to the current interpreter, + " stripping leading indentation and appending a newline + if exists('curinterp') + if type(a:block) ==# v:t_dict + call add(interps_files[curinterp][curfile], {'expand': a:block['expand'], 'contents': (map(block_contents, 'v:val['.nleadingspaces.':]')+[''])}) + else + call extend(interps_files[curinterp][curfile], (map(block_contents, 'v:val['.nleadingspaces.':]')+[''])) + endif + endif + endif + + return interps_files +endfunction " returns all code in the current buffer in the form " {'interpreter': {'file1': [line1, line2], 'file2': [line1, line2]}...} function! s:GetAllCode() - let codelines = {} + " Finalized code, by interpreter and file + let interps_files = {} + + " Code with macros to be expanded, by interpreter and file + " { 'c': [ + " 'output.c': { + " 'toplevel': ['<<whatever>>', ..], + " 'macros': {'<<whatever>>': ["printf('Hello')", ...]}}} + + let macros = {} " The current files set for various interpreters let curfiles = {} @@ -73,27 +139,149 @@ function! s:GetAllCode() " Try to parse the directive let parsedline = s:ParseTangleDirective(curline) - if empty(parsedline) - return {} + let [last_set_interp, should_expand_macros, macro_group, curfile] = parsedline + + " Process file and interpreter declaration + if !empty(curfile) + " Change the current file for the interpreter + let curfiles[last_set_interp] = curfile + + " Process interpreter declaration + " If the interpreter has already been specified + if has_key(interps_files, last_set_interp) + " If the interpreter does not yet have any lines for this file + if !has_key(interps_files[last_set_interp], curfile) + " Add it + let interps_files[last_set_interp][curfile] = [] + endif + " If the interpreter already has lines for the file, don't do anything + " If the interpreter itself hasn't been specified yet + else + " Add it + let interps_files[last_set_interp] = {curfile: []} + endif endif - let [last_set_interp, curfile] = parsedline - " Change the current file for the interpreter - let curfiles[last_set_interp] = curfile - " If the interpreter has already been specified - if has_key(codelines, last_set_interp) - " If the interpreter does not yet have any lines for this file - if !has_key(codelines[last_set_interp], curfile) - " Add it - let codelines[last_set_interp][curfile] = [] + if !empty(should_expand_macros) || !empty(macro_group) + if getline(curline+1)->match(s:codeblock_start) ==# -1 + throw "Tangle directive specifies macros on line " .. curline .. "but no code block follows." endif - " If the interpreter already has lines for the file, don't do anything - " If the interpreter itself hasn't been specified yet - else - " Add it - let codelines[last_set_interp] = {curfile: []} + let block_contents = s:GetBlockContents(curline+1, s:GetBlockEnd(curline+1)) + let block_interp = s:GetBlockInterpreter(curline+1) + + if empty(block_interp) + throw "Macro expansion defined, but no block language set on line " .. curline+1 + endif + + if last_set_interp ==# s:ALL_INTERP + let block_interp = s:ALL_INTERP + endif + + " Process macro expansion + " Top-level macros + if !empty(should_expand_macros) && stridx(should_expand_macros, "^") >=# 0 + if !empty(macro_group) + throw "Top-level macro block on line " .. curline .. " cannot also belong to macro group." + endif + + if has_key(curfiles, block_interp) + let curfile = curfiles[block_interp] + elseif has_key(curfiles, s:ALL_INTERP) + let curfile = curfiles[s:ALL_INTERP] + else + throw "No current file set for block on line " .. curline+1 + endif + + if !has_key(macros, block_interp) + let macros[block_interp] = {} + endif + if !has_key(macros[block_interp], curfile) + let macros[block_interp][curfile] = {} + endif + if has_key(macros[block_interp][curfile], 'toplevel') + throw "Duplicate top-level macro definition on line " .. curline + endif + + " Add the current block as a top-level macro + let macros[block_interp][curfile]['toplevel'] = block_contents + " For regular macro expansion, just add the block + endif + + " Potentially save block as macro + if !empty(macro_group) + " If extending an existing macro + if !empty(should_expand_macros) + let to_add = {'expand': 1, 'contents': block_contents} + else + let to_add = block_contents + endif + if stridx(macro_group, "+") ==# len(macro_group)-1 + let macro_tag = macro_group[1:-3] + if empty(macro_tag) + throw "Macro tag on line " .. curline .. " cannot be empty" + endif + + if has_key(curfiles, block_interp) + let curfile = curfiles[block_interp] + elseif has_key(curfiles, s:ALL_INTERP) + let curfile = curfiles[s:ALL_INTERP] + else + throw "No current file set for block on line " .. curline+1 + endif + + if !has_key(macros, block_interp) + \ || !has_key(macros[block_interp], curfile) + \ || !has_key(macros[block_interp][curfile], 'macros') + \ || !has_key(macros[block_interp][curfile]['macros'], macro_tag) + throw "Requested to extend macro <" .. macro_tag .. "> on line " .. curline .. ", but it's not yet defined" + endif + + if type(to_add) ==# v:t_dict + call add(macros[block_interp][curfile]['macros'][macro_tag], to_add) + else + call extend(macros[block_interp][curfile]['macros'][macro_tag], to_add) + endif + + " If defining a new macro + else + if has_key(curfiles, block_interp) + let curfile = curfiles[block_interp] + elseif has_key(curfiles, s:ALL_INTERP) + let curfile = curfiles[s:ALL_INTERP] + else + throw "No current file set for block on line " .. curline+1 + endif + + let macro_tag = macro_group[1:-2] + if empty(macro_tag) + throw "Macro tag on line " .. curline .. " cannot be empty" + endif + + if !has_key(macros, block_interp) + let macros[block_interp] = {} + endif + if !has_key(macros[block_interp], curfile) + let macros[block_interp][curfile] = {} + endif + + if has_key(macros[block_interp][curfile], 'macros') && has_key(macros[block_interp][curfile]['macros'], macro_tag) + throw "Duplicate definition of macro tag <" .. macro_tag .. "> on line " .. curline + endif + + if has_key(macros[block_interp][curfile], 'macros') + let macros[block_interp][curfile]['macros'][macro_tag] = to_add + else + let macros[block_interp][curfile]['macros'] = {macro_tag: to_add} + endif + endif + endif + + " When processing macros, we process the block also, so move the + " cursor after it + let curline += len(block_contents)+2 endif + " Go to next line let curline += 1 else @@ -106,46 +294,10 @@ function! s:GetAllCode() let block_contents = s:GetBlockContents(curline, s:GetBlockEnd(curline)) if len(block_contents) ==# 0 - echoerr 'No end of block starting on line '.curline - return {} - endif - - " Find out the amount of leading indentation (using the first line) - let nleadingspaces = matchend(block_contents[0], '^ \+') - if nleadingspaces ==# -1 - let nleadingspaces = 0 + throw 'No end of block starting on line '.curline endif - " Get the interpreter for this block - let block_interp = s:GetBlockInterpreter(curline) - if !empty(block_interp) - " Allow overriding all interpreters to a 'general' file: - " If the last Tangle directive didn't have an interpreter, direct - " all blocks to that file - if last_set_interp ==# "" - " Get the current file for 'all interpreters' - let curfile = curfiles[last_set_interp] - let curinterp = s:ALL_INTERP - " If the last Tangle directive specified an interpreter - else - " If the interpreter was specified in a Tangle directive, use its - " current file - if has_key(codelines, block_interp) - let curfile = curfiles[block_interp] - let curinterp = block_interp - " Otherwise, use the 'general' file if specified - elseif has_key(codelines, s:ALL_INTERP) - let curfile = curfiles[s:ALL_INTERP] - let curinterp = s:ALL_INTERP - endif - endif - - " Add the lines to the current file to the current interpreter, - " stripping leading indentation and appending a newline - if exists('curinterp') - call extend(codelines[curinterp][curfile], (map(block_contents, 'v:val['.nleadingspaces.':]')+[''])) - endif - endif + let interps_files = s:AddBlock(interps_files, block_contents, curline, last_set_interp, curfiles) " Skip to after the block let curline += len(block_contents)+2 @@ -156,7 +308,7 @@ function! s:GetAllCode() endif endwhile - return codelines + return [interps_files, macros] endfunction " Write [lines] to fname and open it in a split @@ -191,7 +343,8 @@ function! s:Lang2Interpreter(lang) \ 'python2': ['python2'], \ 'ruby': ['rb', 'ruby'], \ 'sh': ['sh'], - \ 'bash': ['bash'] + \ 'bash': ['bash'], + \ 'cat /tmp/program.c && gcc /tmp/program.c -o /tmp/program && /tmp/program': ['c'], \ } for [interp, langnames] in items(lang2interp) if index(langnames, lang) >= 0 @@ -211,7 +364,7 @@ function! s:GetBlockInterpreter(blockstart) let interp = s:Lang2Interpreter(lang) if empty(interp) - echoerr 'No interpreter configured for language ' . lang + throw 'No interpreter configured for language ' . lang endif return interp @@ -238,7 +391,7 @@ function! s:ClearResult(outputline) call cursor(a:outputline, 1) let resultend = search(s:result_comment_end, 'nW') if resultend ==# 0 - echoerr 'Result block has no end' + throw 'Result block has no end' else execute a:outputline.','.resultend.'delete _' endif @@ -248,21 +401,18 @@ endfunction function! literate_markdown#ExecPreviousBlock() let blockstart = search(s:codeblock_start, 'nbW') if blockstart == 0 - echoerr 'No previous block found' - return + throw 'No previous block found' endif let blockend = s:GetBlockEnd(blockstart) if blockend == 0 - echoerr 'No end for block' - return + throw 'No end for block' endif let interp = s:GetBlockInterpreter(blockstart) if empty(interp) - echoerr 'No interpreter specified for block' - return + throw 'No interpreter specified for block' endif let block_contents = s:GetBlockContents(blockstart, blockend) @@ -276,29 +426,111 @@ function! literate_markdown#ExecPreviousBlock() call append(outputline-1, ['RESULT:'] + result_lines + ['-->']) endfunction -function! literate_markdown#Tangle() - " Get all of the code blocks in the file - let lines = s:GetAllCode() - - " If there's any, tangle it - if len(lines) ># 0 - " Merge lines from all interpreters into the files - let all_interps_combined = {} - for fname_and_lines in lines->values() - for [fname, flines] in fname_and_lines->items() - if all_interps_combined->has_key(fname) - call extend(all_interps_combined[fname], flines) +function! s:ExpandMacro(macros, interp, fname, line) + let nleadingspaces = matchend(a:line, '^ \+') + if nleadingspaces ==# -1 + let nleadingspaces = 0 + endif + + let macro_tag = trim(a:line)[2:-3] + let expanded = [] + if !has_key(a:macros[a:interp][a:fname]['macros'], macro_tag) + throw "Macro " .. macro_tag .. " not defined for file " .. a:fname + endif + + let expansion = a:macros[a:interp][a:fname]['macros'][macro_tag] + if type(expansion) ==# v:t_dict + let expansion = [expansion] + endif + for expanded_line in expansion + if type(expanded_line) ==# v:t_dict && expanded_line['expand'] + for l in expanded_line['contents'] + if l->trim()->match('<<[^>]\+>>') >=# 0 + call extend(expanded, s:ExpandMacro(a:macros, a:interp, a:fname, repeat(" ", nleadingspaces)..l)) else - let all_interps_combined[fname] = flines + call add(expanded, repeat(" ", nleadingspaces)..l) endif endfor - endfor + else + call add(expanded, repeat(" ", nleadingspaces)..expanded_line) + endif + endfor - " Loop through the filenames and corresponding code - for [fname, flines] in items(all_interps_combined) - " Write the code to the respective file - call s:SaveLines(flines, fname) - endfor - endif + return expanded +endfunction + +function! s:ProcessMacroExpansions(lines, macros) + let final_lines = {} + for [interp, fnames] in a:lines->items() + if has_key(a:macros, interp) + for [fname, flines] in fnames->items() + if has_key(a:macros[interp], fname) + if !has_key(a:macros[interp][fname], 'toplevel') + throw "Macros exist, but no top-level structure defined for file " .. fname + endif + + let toplevel = a:macros[interp][fname]['toplevel'] + let lines_here = [] + for line in toplevel + if line->trim()->match('<<[^>]\+>>') >=# 0 + call extend(lines_here, s:ExpandMacro(a:macros, interp, fname, line)) + else + call add(lines_here, line) + endif + endfor + + if !has_key(final_lines, interp) + let final_lines[interp] = {fname: lines_here} + else + let final_lines[interp][fname] = lines_here + endif + else + if !has_key(final_lines, interp) + let final_lines[interp] = {fname: a:lines[interp][fname]} + else + let final_lines[interp][fname] = a:lines[interp][fname] + endif + endif + endfor + else + let final_lines[interp] = a:lines[interp] + endif + endfor + return final_lines +endfunction + +function! literate_markdown#Tangle() + " Get all of the code blocks in the file + try + let [lines, macros] = s:GetAllCode() + + if !empty(macros) + let lines = s:ProcessMacroExpansions(lines, macros) + endif + + + " If there's any, tangle it + if len(lines) ># 0 + " Merge lines from all interpreters into the files + let all_interps_combined = {} + for fname_and_lines in lines->values() + for [fname, flines] in fname_and_lines->items() + if all_interps_combined->has_key(fname) + call extend(all_interps_combined[fname], flines) + else + let all_interps_combined[fname] = flines + endif + endfor + endfor + + " Loop through the filenames and corresponding code + for [fname, flines] in items(all_interps_combined) + " Write the code to the respective file + call s:SaveLines(flines, fname) + endfor + endif + catch + echoerr v:exception + endtry endfunction diff --git a/doc/literate-markdown.txt b/doc/literate-markdown.txt @@ -6,11 +6,14 @@ License: Same terms as Vim itself (see |license|) INTRODUCTION *literate-markdown* Literate programming is the interleaving of code with explanation in natural -language, with the ability of generating just the executable code from the -file. People often know it from Org mode in Emacs; this plugin aims to bring -similar functionality to Markdown files in Vim. It will let you execute blocks -of code (delimited by three backticks in Markdown) and extract these blocks of -code into separate files through tangling. +language, in any order, with the ability of generating just the executable +code from the file. People often know it from Org mode in Emacs; this plugin +aims to bring similar functionality to Markdown files in Vim. It will let you +execute blocks of code (delimited by three backticks in Markdown) and extract +these blocks of code into separate files through tangling. With macros, you +can document your code without needing to care about the order in which +information is presented -- the tangler will extract code in the correct +order. COMMANDS *literate-markdown-commands* @@ -63,6 +66,38 @@ overrides all previous directives for that language for the remainder of the file. A directive with no language specified overrides all previous directives for the remainder of the file. +You can use macros. A macro is limited to the specified file and language, it +is not accessible from other files/languages. To specify that macros should be +expanded within a block, put "<>" in the block's tangle directive: > + <!-- :Tangle(language) <> /path/to/output/file.ext --> +< + + +Any files that use macros should have a top-level block (i.e. one that is not +contained in any other macro); this block should have "<^>" in its tangle +directive: > + <!-- :Tangle(language) <^> /path/to/output/file.ext --> +< + +A macro in a code block has to be on its own line (preceded only by +whitespace), and is written in double angle brackets: > + <<this is a macro>> +< + +A code block that defines the macro includes this macro in its tangle +directive, in single angle brackets: > + <!-- :Tangle(language) <this is a macro> /path/to/output/file.ext --> +< + +The closing angle bracket in the macro name can optionally be followed by a +plus sign, which appends the contents of the block the the existing definition +of the macro: > + <!-- :Tangle(language) <this is a macro>+ /path/to/output/file.ext --> +< + +Please see the examples below, and the examples directory in the root of this +repository, for more explanation of usage. + Examples~ Below are some examples of Markdown code block tangling, followed by @@ -119,6 +154,60 @@ by the second Python-specific directive, so it will be tangled to will not be tangled. The fourth (Bash) code block is affected by the first (generic) tangle directive, so it will be tangled to /tmp/generic-file. + +> + <!-- :Tangle(python) <^> /tmp/python.py --> + ```python + <<function definitions>> + + def main(): + <<main code>> + + if __name__ == '__main__': + main() + ``` + + <!-- :Tangle(python) <function definitions> --> + ```python + def double(n): + x = 2*n + return x + ``` + + <!-- :Tangle(python) <> <main code> --> + ```python + <<n definition>> + print("Double of %d is %d" % (n, double(n))) + ``` + + <!-- :Tangle(python) <n definition> --> + ```python + n = 34.5 + ``` +< + +The first Python block defines the overall structure, so it's the top level +macro block (indicated by "<^>" in its tangle directive). It makes use of two +macros, "function definitions" and "main code". The second block defines the +"function definitions" macro; it does not contain any more macros, so it does +not have "<>" in its tangle directive. The third block defines the "main code" +macro, and contains another macro to be expanded ("n definition"), so it has +"<>" in its tangle directive. The final block defines the "n definition" macro +and does not contain any other macros. The resulting code will be > + def double(n): + x = 2*n + return x + + def main(): + n = 34.5 + print("Double of %d is %d" % (n, double(n))) + + if __name__ == '__main__': + main() +< + + + CODE EXECUTION *:ExecPrevBlock* *literate-markdown-exec* *<Plug>LitMdExecPrevBlock* diff --git a/examples/myfact.md b/examples/myfact.md @@ -0,0 +1,99 @@ +# myfact: a C implementation of a factorial function +This file describes a C implementation of a factorial function. +It generates three files: + +* `myfact.c`: the implementation of factorial +* `myfact.h`: the header file for factorial +* `main.c`: the main file, which includes and runs the factorial function + +This is clearly over-engineered; it aims to only be an example of the power of literate programming. + +## The main file +The main file is pretty simple: + +<!-- :Tangle <^> main.c --> +```c +<<main imports>> +int main() { + <<call the function from the header file>> + return 0; +} +``` + +As you can see, it just calls whatever we define in the header. + +Now for the header -- let's do a factorial calculation. + +## Factorial calculation +The factorial function is recursive. +We'll make use of the following mathematical definitions: + +``` +0! = 1 +n! = n × (n-1)! +``` + +Let's have a basic definition of the function like this: + +<!-- :Tangle <> <factorial function> myfact.c --> +```c +int myfact(int n) { + <<base case>> + <<recursive case>> +} +``` + +The base case is when n == 0: the result is 1. +Putting that into code: + +<!-- :Tangle <base case> myfact.c --> +```c +if (n == 0) return 1; +``` + +Going from the mathematical definition, the recursive case calls the factorial function. +Like this: + +<!-- :Tangle <recursive case> myfact.c --> +```c +return n*myfact(n-1); +``` + +The implementation file as a whole has this structure: + +<!-- :Tangle <^> myfact.c --> +```c +// No imports necessary +<<factorial function>> +``` + +And its corresponding header file just has the prototype: + +<!-- :Tangle myfact.h --> +```c +int myfact(int); +``` + +## Calling it from the main file +Back to the main file now. +We've written our factorial function in `myfact.h`. +So, first, we need to `#include` our header file: + +<!-- :Tangle <main imports> main.c --> +```c +#include "myfact.h" +``` + +Then we need to call the factorial in the body of the main function: + +<!-- :Tangle <call the function from the header file> main.c --> +```c +printf("The factorial of %d is %d\n", 5, myfact(5)); +``` + +Of course, to be able to print, we need to include the IO header: + +<!-- :Tangle <main imports>+ main.c --> +```c +#include <stdio.h> +```