From aaa5209905fd5af3e0083518729608f9a248aa6e Mon Sep 17 00:00:00 2001 From: Cody Boone Ferguson <53008573+xexyl@users.noreply.github.com> Date: Sun, 12 Nov 2023 08:19:54 -0800 Subject: [PATCH] Updates and fixes to 1998/fanf The try.sh script has been added to run the various inputs the judges recommended one try. The Makefile now can do the intermediate steps. This is possible with the way I fixed the entry to compile. If you look at the original code it's different than the fixed version which is post processing (if I recall correctly). Updated .gitignore. Unless there's a mistake in markdown formatting this should complete 1998/fanf's review. --- 1998/fanf/.gitignore | 2 + 1998/fanf/Makefile | 6 ++- 1998/fanf/README.md | 97 +++++++++++++++++++++----------------------- 1998/fanf/try.sh | 41 +++++++++++++++++++ thanks-for-fixes.md | 10 +++-- 5 files changed, 100 insertions(+), 56 deletions(-) create mode 100755 1998/fanf/try.sh diff --git a/1998/fanf/.gitignore b/1998/fanf/.gitignore index f98d7a426..40e42a6a6 100644 --- a/1998/fanf/.gitignore +++ b/1998/fanf/.gitignore @@ -1,5 +1,7 @@ fanf fanftmp1.c +fanftmp1 fanftmp2.c +fanftmp2 fanf.orig prog.orig diff --git a/1998/fanf/Makefile b/1998/fanf/Makefile index e2827bad4..a8c657259 100644 --- a/1998/fanf/Makefile +++ b/1998/fanf/Makefile @@ -130,8 +130,10 @@ all: data ${TARGET} sandwich supernova deep_magic magic charon pluto ${PROG}: ${PROG}.c - @echo "This next step may take some time to complete:" - ${CC} ${CFLAGS} ${PROG}.c ${LDFLAGS} -o $@ + @echo "This may take some time to complete:" + ${CC} ${PROG}.c -E > ${PROG}tmp1.c + ${CC} ${PROG}tmp1.c -E > ${PROG}tmp2.c + ${CC} ${CFLAGS} ${PROG}tmp2.c ${LDFLAGS} -o $@ # alternative executable # diff --git a/1998/fanf/README.md b/1998/fanf/README.md index 9d412af24..5d4e7036d 100644 --- a/1998/fanf/README.md +++ b/1998/fanf/README.md @@ -4,7 +4,7 @@ make all ``` -NOTE: This may take a while. Some systems may have problems building\ +NOTE: this may take a while. Some systems may have problems building this entry because of the system resources it requires. @@ -15,36 +15,26 @@ this entry because of the system resources it requires. ``` -### To use: +### Try: -Enter an expression on standard input. Here are some -sample expressions that you can use: +Enter an expression on standard input. To try some we have selected: -``` -((\a(\b(\c(d)))) e) - - -(\f\g\x - ( - (g x) (f x) - ) K K z -) - - -(\f(\f\g\x( (f((\a(g(b))) e)) (g x) ) K K z)) - - -(Y\f\n - ((= n 0) - 1 - (* n (f (- n 1))) - ) -) +```sh +./try.sh ``` ## Judges' remarks: + +### Historical aside: + +At a time the code in [fanf.c](fanf.c) was that of [fanf.orig.c](fanf.orig.c) +but to get this to compile in modern systems it had to be translated to what you +now see. The intermediate steps can still be performed but they might be +different from the past. This should be kept in mind as you read the below +remarks. + This program translates lambda expressions into combinator expressions. But you do not need to know Lambda Calculus to be impressed by this program! @@ -56,20 +46,21 @@ Notice how large the code grows from the [fanf.c](fanf.c) into the final `fanftmp2.c` C program. Take a look at that final C program again, can you begin to understand what it is doing? -Look at the first stage of the C pre-processing: +Look at the first stage of the C pre-processing which was done like: ```sh cc fanf.c -E > fanftmp1.c ``` -Skip to the bottom of `fanftmp1.c` (after all of the `#include` header -stuff ... look for a line of the form: `# 2 "fanf.c"` near the bottom) +Skip to the bottom of `fanftmp1.c` (after all of the C pre-processor header +stuff ... look for a line of the form: `# 38 "fanf.c"` near the bottom) and look at the resulting code. This code, when C pre-processed will expand into over 342 times the original code (ignoring `#include` headers) to produce `fanftmp2.c`. That program in turn, when compiled translates a single lambda expression on standard input into combinator expressions on standard output. + ### Extra credit question: What do you have to do to make this program work with an old non-ANSI C @@ -88,7 +79,7 @@ for translating lambda expressions into combinator expressions. The syntax of lambda expressions recognised by the program is as follows: The basic atoms are variables which are single characters other than backslash or parentheses, e.g. `a` or `b` or `@`. Variables are -combined by function application which is written e.g. `(f a)` which is +combined by function application which is written as e.g. `(f a)` which is the function `f` applied to the argument `a`. Application groups to the left, so `(f a b c)` is equivalent to `(((f a) b) c)`. Functions are created from lambda terms which are written in the form `\a(expr)` which @@ -131,30 +122,34 @@ The algorithm for translating lambda expressions into combinator expressions works as follows. There are three forms of lambda expression to consider: variables, applications, and abstractions. -trans `v` -> `v` (variable) -trans `a b` -> (trans `a`) (trans `b`) (application) -trans `\ab` -> abs a (trans `b`) (abstraction) +``` +trans v -> v (variable) +trans a b -> (trans a) (trans b ) (application) +trans \ab -> abs a (trans b) (abstraction) +``` There are a further three cases to consider for the body of lambda expressions, where we need to do the magic that transforms away the variables. -abs a `f x` -> `S` (abs a `f`) (abs a `x`) -abs a `b` -> `K b` (b != a) -abs a `a` -> `I` +``` +abs a f x -> S (abs a f) (abs a x) +abs a b -> K b (b != a) +abs a a -> I +``` E.g. suppose we had combinator expressions for `+` and `3` and we wanted to see what the combinator expression for doubling 3 looked like: ``` - trans `\x(+ x x) 3` --> (trans `\x(+ x x)`) (trans `3`) --> (abs x (trans `+ x x`)) `3` --> (abs x `+ x x`) `3` --> `S` (abs x `+ x`) (abs x `x`) `3` --> `S` (`S` (abs x `+`) (abs x `x`)) `I` `3` --> `S` (`S` (`K +`) `I`) `I` `3` --> `S (S (K +) I) I 3` + trans \x(+ x x) 3 +-> (trans \x(+ x x)) (trans 3) +-> (abs x (trans + x x)) 3 +-> (abs x + x x) 3 +-> S (abs x + x) (abs x x) 3 +-> S (S (abs x +) (abs x x)) I 3 +-> S (S (K +) I) I 3 +-> S (S (K +) I) I 3 ``` We can then check that this evaluates to the expected result: @@ -176,7 +171,7 @@ same as `S (K a) (K b)`, because -> a b ``` - and +and ``` S (K a) (K b) x @@ -220,7 +215,7 @@ It is fairly well known that the lambda calculus (and hence SK combinators) can compute anything, but mere computation is no use if you cannot communicate with the world. This is why `OFL` includes a few concessions to reality: a combinator `E` for representing characters and -testing them for equality, two IO combinators, `G` and `P`, for reading +testing them for equality, two I/O combinators, `G` and `P`, for reading and writing characters respectively, and a combinator `J` for representing `false`. I also implemented the `Y` combinator directly rather than in terms of the primitive combinators, since it only @@ -239,7 +234,7 @@ the expression evaluates to `J`. `K` and `J` are used to represent `true` and `false` respectively; they correspond exactly to the standard lambda calculus representations of -true and false, viz. `\t\f(t)` and `\t\f(f)` respectively. In this way a +`true` and `false`, viz. `\t\f(t)` and `\t\f(f)` respectively. In this way a conditional expression can just be written `cond then else`, which is even more terse than C `cond ? then : else`. An expression that compares a character with `a`, say, can be written in the rather @@ -247,7 +242,7 @@ obfuscated form `E('a') char then else`. The `P` combinator, like the `E` combinator, starts off by evaluating its first argument and checking that it is some form of `E`. It then -writes that character on stdout, and finally it calls its second +writes that character on `stdout`, and finally it calls its second argument as a function with the argument `I`, i.e. ``` @@ -256,7 +251,7 @@ P x f -> f I with a side-effect. -The `G` combinator just reads a character from stdin then calls its +The `G` combinator just reads a character from `stdin` then calls its argument as a function with the appropriate `E` combinator as an argument, i.e. @@ -268,12 +263,12 @@ with a side-effect. One of the problems with non-strict languages is that it is hard to predict in advance in which order side-effects will occur. In my program -I used a monadic IO structure like that used by the programming language +I used a monadic I/O structure like that used by the programming language Haskell; this allows one to write programs that manipulate external state in a manner remarkably similar to imperative programming languages like C. An examination of my program should provide an illustration of how well this technique works in practice. (In particular, I used the -CPS form of the IO monad from page 6 of "Imperative Functional +CPS form of the I/O monad from page 6 of "Imperative Functional Programming".) The `Y` combinator is used for implementing recursive functions. It has @@ -342,8 +337,8 @@ of the program that is then interpreted. The first pass of the compiler is implemented via the C preprocessor. It implements one optimisation, namely function in-lining. Unfortunately, this optimisation always increases object code size and execution time. -The opposite optimisation, common sub-expression elimination, (which -would offer improvements in code size and execution time) has not been +The opposite optimisation, common sub-expression elimination (which +would offer improvements in code size and execution time), has not been implemented. The output of this pass is a C program that may be compiled by a normal C compiler. diff --git a/1998/fanf/try.sh b/1998/fanf/try.sh new file mode 100755 index 000000000..8622bfb10 --- /dev/null +++ b/1998/fanf/try.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +make all || exit 1 + +echo "$ echo '((\a(\b(\c(d)))) e' | ./fanf" 1>&2 +echo '((\a(\b(\c(d)))) e)' | ./fanf +echo 1>&2 + +echo "$ echo ' +(\f\g\x + ( + (g x) (f x) + ) K K z +)'|./fanf" 1>&2 +echo ' +(\f\g\x + ( + (g x) (f x) + ) K K z +)'|./fanf +echo 1>&2 + + +echo "$ echo '(\f(\f\g\x( (f((\a(g(b))) e)) (g x) ) K K z))' | ./fanf" 1>&2 +echo '(\f(\f\g\x( (f((\a(g(b))) e)) (g x) ) K K z))' | ./fanf +echo 1>&2 + +echo "$ echo ' +(Y\f\n + ((= n 0) + 1 + (* n (f (- n 1))) + ) +)' | ./fanf" 1>&2 + +echo '(Y\f\n + ((= n 0) + 1 + (* n (f (- n 1))) + ) +)' | ./fanf diff --git a/thanks-for-fixes.md b/thanks-for-fixes.md index 9772e1152..56584897b 100644 --- a/thanks-for-fixes.md +++ b/thanks-for-fixes.md @@ -2050,9 +2050,13 @@ exiting the program. ## [1998/fanf](1998/fanf/fanf.c) ([README.md](1998/fanf/README.md)) -Cody fixed this for Linux. The problem was the intermediate steps to get to the -final code that is compiled. The entry itself is what was essentially what used -to be compiled. Now it works with both Linux and macOS (and BSD?). +Cody fixed this to compile. The problem was the intermediate steps to get to the +final code that is compiled. The code is now what it essentially becomes when +processed completely. The intermediate steps can now be performed to see how it +expands but it can still compile and be used. + +Cody also added the [try.sh](1998/fanf/try.sh) script to show the output of some +of the expressions that we selected. ## [1998/schnitzi](1998/schnitzi/schnitzi.c) ([README.md](1998/schnitzi/README.md]))