Showing posts with label retro. Show all posts
Showing posts with label retro. Show all posts

23 April 2024

39 Years of Coding

39 (licensed CC BY by Tim Pierce)Last week was my 39th anniversary of coding. I got a Commodore 64 as a present for Easter Sunday from my mother. I own an old image to prove that. The exact date is tricky: There are no time stamps on my files, I do not know how old my oldest programs are. I wish I had added comments with time stamps back then. I was able to pin down some programs, like demos, to the year they were created by investigating file names and scrolling messages. On of these attributes to Easter 1986, so my start must have been in 1985. One of my neighbours had made fun of people using the Commodore only to play games, and I had bought a book about coding BASIC even before I owned the machine. It was a nice book with exercises to write pieces of code into gaps in the text. I imagine I wrote some silly Hello World program right away on that Easter Sunday, which would have been 8th of April 1985.

BASIC
I spent a large part of my teens fiddling with the Commodore 64 and its BASIC. It will always have a special place in my heart. I never really left BASIC behind. From time to time, I code a little kata in the emulator, e.g. Prime Factors, Game of Life or several years later Roman Numerals. When learning a new programming language, my usual exercise is to create a Scheme (Lisp) interpreter, but I have also played with BASIC as a Scala DSL and even turned it upside down creating a BASIC parser in Scheme, using TDD, unit tests and a file watcher to run my tests for all modified files. It was a fun project and I stopped after parsing most of the BASIC code I was able to find on my old disks.

Monkeys Everywhere
So what did I do on the evening of my anniversary? I opened a can of energy drink and had a look at a new programming language. I went for Garmin Connect IQ, a platform by Garmin to build applications for their watches. While I did not own a Garmin device, I wanted to support a developer at my client who wanted to create her own specialised app for her watch.

Chimpanzee (licensed CC BY by William Warby)Connect IQ reminds me a lot of Android: There is an SDK, support for various devices, an emulator, an API for different kinds of apps, an app store with review process and so on. It has manifests, permissions, storage, intents etc. Someone at Garmin had some humour, as the programming language is called Monkey C (with extension .mc), the build system is called Jungles (.jungle) and the system libraries use the Toybox namespace. They even have their own domain-specific property language for managing style elements, derived from CSS. The whole thing is branded with monkeys all over the place.

Using a small, proprietary language has disadvantages: There are only few public code samples to copy from and ChatGPT is unable to create any working code in Monkey C. Still I found everything I needed during that first evening: a minimalist Unit Testing framework and CLI commands to build and test my code. Piping the test output through a small shell script added ANSI colours, i.e. Red and Green respectively, to the test output. Perfection! In my tradition of learning new languages, I TDD'ed the Prime Factors code kata as first exercise:
import Toybox.Lang;

class PrimeFactors {

  static function generate(n as Integer) as Array<Integer> {
    var factors = [] as Array<Integer>;
    
    for (var candidate = 2; candidate <= n; candidate++) {
      while (n % candidate == 0) {
        factors.add(candidate);
        n = n / candidate;
      }
    }

    return factors;
  }

}
The language itself is object oriented and looks a lot like JavaScript with optional types, the as ... clauses. It is a compiled language and all type declarations are optional but can be forced with a compiler flag. I felt at home immediately. What a happy anniversary ;-)

26 February 2022

Porting RLE8

I am going back in time (again) and playing with some of my old source code: In the days of MS-DOS, 16 bit operating systems and 640 kB of RAM, I created a whole bunch of MS-DOS utilities and tools, mostly written in Turbo Pascal. In the last 20 years I have only ported a few of them. From time to time I miss one of these old tools - but never miss it enough to invest the time to write it from scratch. Last year I came across p2c, a Pascal to C translator by Dave Gillespie. Oh such joy - and I used it to port my old tools. One tool I used - which I had not created myself - was RLE8 by Shaun Case, Public Domain 1991.

art Moderne (licensed CC BY-NC-ND by Nadine)RLE8
RLE stands for run-length encoding compression. It is a form of data compression in which repeated values are represented by a count and a single instance of the value. It is a very simple form of compression and in the nineties it sometimes reduced disk and memory space significantly. It was used in early versions of Windows BMP for bitmap compression named BI_RLE8: An RGB format that used run-length encoding (RLE) compression for bitmaps with 8 bits per pixel. The compression used a 2-byte format consisting of a count byte followed by a byte containing a colour index. There were versions for 4 and 8 bit image data, RLE4 and RLE8 respectively. "RLE compression was used in the stone-age" as one forum comment reads and today there not much interest in it. The only reference I found was for benchmarking highly optimised code.

Finding the Original Source
Finding the original source code was difficult. It is in the nature of the modern WWW that new pages appear and old ones disappear. Fortunately the RLE8.EXE printed the name of its author and its license: Shaun Case 1991 Public Domain. After some googling I found an article about it, which later turned out to be the contents of the Readme someone had reposted almost ten years later on GameDev. Eventually I found the Retro Computing Archive with its collection of CD-ROMs containing shareware and Public Domain software from the late 80's and 90's. RLE8_SC. Win! (The Retro Computing Archive is great, many of its ZIP files are unpacked and therefore crawled by Google which helped me find it.)

Porting from Turbo C
The code compiled without issues, but included a header I did not have, dir.h, a header from Borland's Turbo C. I guess this is the biggest issue when porting old code - calls to non-standard library functions. I missed fnsplit, a function which split file specifications into component parts. While I could have created the function myself - an excellent opportunity to practice my C - I searched more and luckily someone had created it already. Thank you Robert B. Stout. Robert granted license to use his source files to create royalty-free programs, which RLE8 is. After adding some more of Robert's code, and removing code which was not needed, RLE8 compiled and linked, even with my pedantic settings of gcc -std=c99 -pedantic -pedantic-errors -Wall -Wextra. I loved it. Witness the power of C, a 50 years old programming language, still moving forward.

Download original and modified sources together with binaries for DOS and Windows x86.

29 November 2017

Interview Rea

Palestinians Collect Belongings from Gaza RuinsThe last Coderetreat was one of the rare Coderetreats that I was participating instead of organizing. I used the opportunity to pair with strangers and Rea was one of my partners. We came to talk about the sponsor of the Coderetreat and if she would work for a company offering products in the defence sector. So I asked her to answer my usual questions. These were her answers:

Rea, why did you choose to become a software developer?
I consider myself a beginner in the field of software development. I chose to switch to software development because in this world where computers play an increasingly important role I don't want to be a bystander, but be able to help shape it. I believe that in the near future software development should be as basic a skill as writing and arithmetic.

You said that you would not work for a company in the defence industry. Why is that so?
Call it defence industry or call it arms industry - its goal is to build something that harms or kills other people. Calling it defence industry rather than attack industry is just a euphemism. In the end it doesn't matter who started it. No defence industry is meant do be just passive-defensive. It always includes active attack as well. I don't want to have a part in this. I don't see why some humans should be worth living, while others can be harmed, maimed, crippled or killed at the whim of somebody in an office somewhere far from the actual action. Even thinking that there is a state funded - and therefore tax payer funded - industry that has the goal to kill makes me sick.

What other topics are you concerned about and what do you do about them?
I am concerned about many topics: Microbial resistance, climate change, loss of privacy, growing fear that manifests itself in hatred against others. I am not a politician and don't want to be one. What I do is to think hard about what I, as a single human being, can do, and then take little steps, one at a time. Even if they are not the most comfortable steps. Doing something, as little as it may be, is more valuable than to despair and do nothing at all. "Many a mickle makes a muckle" - I am convinced that each and every little step counts.

To be more specific, I do my little share against climate change, try to reduce waste and emissions, and have a small footprint. Although I have given up being a strict vegetarian (which I was for about 20 years), I eat meat less than once a month. I don't own a car and try to avoid using one as often as possible. I don't think it is necessary to travel extensively, although I really would love to see the world. I just think the harm it does is worse than the benefits I get. I buy locally and seasonally whenever possible. For me, this has precedence over organic food.

I have recently read that the volume of insect biomass has declined by almost 80% in the last 40 years in areas where it was measured, and other areas are likely to have seen a similar decline. This frightens me. Especially as the reasons can only be speculated about, and there is nothing I can do.

I speak up against racism, homophobia, sexism, etc. when I encounter it. I embrace diversity and try to raise awareness in others. I try not to judge what I don't know. Often, its not a matter of 'good' versus 'bad', but just of 'different'.

As for loss of privacy, again, I try to raise awareness. When people tell me they don't mind being under surveillance, I tell them this: You have nothing to hide when you go to the bathroom. But I bet you would rather not have me watch you there. I don't own a smart phone and still resist the urge to buy one. I try to reduce being tracked while surfing the Internet. I don't want a smart TV or any other smart device in my apartment. I still have an email address with Google, this is something I want to change soon. I do quite well without most of social media. And I wonder again and again how much longer I can keep up this retro lifestyle.

Methicillin-Resistant Staphylococcus aureus BacteriaWhat do you consider the biggest challenges for humanity at the moment?
I think bacteria resistance to antibiotics is one of the biggest dangers we are facing. This, combined with today's mobility, is all you need for a ghastly Hollywood movie scenario. I hope reality won't be as bad.

What could we do to engage in topics like meat mass production or pollution? For example, did you take part in public protests, donate money to NGOs or sign petitions?
As to what we can do - see my answer above. Signing petitions with Avaaz is something I did for a while. I realised that doing this makes me lazy in other ways. Signing gives me this feeling of accomplishment, but there is more I can do, in my own vicinity. The best way to fight meat mass production is not to consume this meat. For some people this is a bigger departion from their habits than for others, granted. But if you skip meat once a week, this is better than nothing. Or skip every other day. Or have meat once a week only. Just reduce your consumption. I think the difference in production cost between vegetables and meat should be reflected in the price. Not only the monetary price, but also the price as concerns the ecological footprint.

I have a split relationship with NGOs. I used to support Greenpeace, for example, but don't do that any more. One of the reasons is that I strongly disagree with their campaign against GMOs. Let's not get too deep into this here. But I do support NGOs as such, I think it is important that there are organisations that are independent of governments and other big influencers.

Do you think it is possible to work on "the right things"?
This is not my own idea, and I don't remember where I heard it first, but it had a great effect on me: Thinking about how much good I can do with my own work and how much good can be done with the money I earn, I have to realise that my money can do more than I can. While I would prefer to do both - work a job that matters and give money to others to do good stuff, I am not currently in the position to do so. I find it hard enough to avoid doing things I am not comfortable doing, working for industries I would rather not support. But what I can do is live a frugal life and give away the money I don't need to help others do better things. Again, not because I don't want to get my hands dirty or anything, but because other people or organisations are better suited to doing this stuff.

I wish I could do both: work on the things I believe to be the right ones, and support others to do the same at the same time. But then again, what I believe to be the right things might turn out to be completely wrong. I think it is important to keep an open mind on what the right things are, and change directions in the face of new evidence.

If it happened, please tell the story of your decisions regarding your work because of your values and social responsibility.
I don't remember any such decisions in my short life as a developer. I imagine that they would include security or privacy issues, or issues of inclusion/exclusion of certain groups. Before becoming a developer, I worked at a staffing firm for a very short time. After about a month I figured out how they treated their staff and quit immediately. I did not want to be associated in any way with such business practices. In hindsight I regret not having done more for the staff.

How do you think about selecting industry, customer and project based on your values?
An important choice for me was to work with a company that embraces Open Source Software instead of working for a closed source company. However, the software we produce is not Open Source, which I deeply regret. Choosing the customers I work with directly is unfortunately out of reach at the moment. This is something I aspire to.

Do you have problems with any industries? Why? What about Porn industry or weapon manufacturing?
I don't think porn is bad per se, as long as it is consenting adults working in it. (Of course, more could be said about the gender issues in most porn, but this is not the place and time for that.) As for the industries I don't want to work for: I don't have a list ready. But it is very obvious for me that the weapons and defence industry definitely have place high on that list. I would put animal factories and suppliers on that list too. As a teen I had to work as a farm hand for a few weeks, and was assigned to a small scale chicken farm. Every morning I had to walk through the big hall with all the chicks and collect the ones that had choked over night. Not a good memory.

Cow PortraitObviously, companies that use child labour are out of question as well. I also see combustion engines as a problem for climate change - although there are bigger problems, and worse polluters. Many car companies are looking into ways of making individual transport more sustainable making it a less clear-cut question. On the other hand, I would not want to work for companies in the petrochemical sector. Banks and insurance companies probably get a space on that list as well. And the pseudo-pharma industry that produces and sells pseudo-medicine. I mean those who sell sugar beads or other quackery and convince people they are as good as - or even better than - proper medicine. Any company, basically, that is concerned more with benefits for the few, instead of adding value for the many. Which is, unfortunately, quite common practice.

Did you ever reject a customer based on your values?
As I mentioned in an earlier question, I quit a job I actually needed because of fraudulent behaviour on their side.

On the other hand, what would be projects that you would love to work on?
I would love to work for social and educational projects. I see education as the most important tool to build a better world. Education on all levels, and for all social groups, everywhere. I am convinced that education is the only way to solve the problems we have today. (This is one of the reasons why I don't want to give up my small teaching gig.)

Some branches of research are important too. No future development without research. Not related to any specific project or customer, I find the use, propagation, and development of Open Source Software to be of great importance. Apart from the often mentioned four freedoms, I see it as a means to more equality. I have used and propagated Open Source for a long time, and want to start contributing to it as my next goal.

Thank you Rea for taking the time to answer my questions. (I saw the time of your last email was 1:18 am.)
Thank you, Peter, for the opportunity to think about these topics in a structured way! If anyone has any suggestions, please let me know.

14 February 2010

Turbo Pascal Prime Factors Kata

Wow Retro Recently I started performing the Prime Factors Kata. After playing with Java and Ruby I had the weird idea of performing it in every programming language I ever knew. Well, maybe not every - I don't plan to use 6502 or 80x86 assembler, that wouldn't be fun. But I already did it in BASIC. Going forward in time Turbo Pascal would be next. So it's time for another retro post ;-)

Where Is My TPUnit?
I couldn't find any unit testing framework for Turbo Pascal. The closest thing I could find was FPCUnit packaged with Free Pascal. Unfortunately it's not compatible with good old TP. So I had to roll my own. I started with some minimalist infrastructure.
TYPE TestCase = OBJECT
    PROCEDURE AssertEquals(msg:String; expect, act:Longint);
    PROCEDURE AssertNil(msg:String; act:Pointer);
    { other asserts ... }
    PROCEDURE Fail(msg:String);
    { TestCase }
    PROCEDURE SetUp; VIRTUAL;
    PROCEDURE TearDown; VIRTUAL;
  END;

PROCEDURE TestCase.AssertEquals(msg:String; expect, act:Longint);
VAR ex, ac:String;
BEGIN
  IF expect <> act THEN
  BEGIN
    Str(expect, ex);
    Str(act, ac);
    Fail(Concat(msg,' expected ',ex,' but was ',ac));
  END;
END;

...

PROCEDURE TestCase.Fail(msg:String);
BEGIN
  TearDown;
  Writeln(' - FAILED');
  Writeln(msg);
  Halt(1);
END;

PROCEDURE TestCase.SetUp;
BEGIN
END;

PROCEDURE TestCase.TearDown;
BEGIN
END;
Subclasses may overwrite SetUp or TearDown, add test methods and call Asserts. To keep it simple the first failed assertion stops program execution. What's missing is some kind of procedure RunTest that would wrap a particular test method inside calls to SetUp and TearDown. Hmm - function variables might be handy here. In case you are not familiar with them, here is an example:
{$F+} {needs far calls for function variables}
TYPE FuncVar = PROCEDURE;

PROCEDURE FancyMethod(method:FuncVar);
BEGIN
  method; { invokes the method }
END;

PROCEDURE SomeMethod;
...

VAR v:FuncVar;
BEGIN
  v := SomeMethod;
  FancyMethod(v);
END.
Pascal's CalculatorUnfortunately Turbo Pascal does not allow class methods (e.g. TestCase.AssertEquals) to be used as function variables. (At least I couldn't figure.) Obviously self is an implicit parameter of all such methods. Well not obviously, but analysing the generated machine code helps ;-)
SomeMethod; { -> 0E E8 D1 FB }
push CS { because of $F+ }
call fbe1 { address of SomeMethod in CS }

cls.ClassProc; { -> BF 70 00 1E 57 0E E8 ED FB }
mov DI, #0070 { address of object cls in DS }
push DS
push DI { first parameter is self }
push CS
call fbed { address of ClassProc in CS }
Using this knowledge TestCase.RunTest is implemented a bit dirty using an untyped Pointer argument:
PROCEDURE CallClassPtr(pt:Pointer; VAR cls:TestCase);
VAR s,o:Word;
BEGIN
  s := Seg(cls);
  o := Ofs(cls);
  ASM
    mov DI, [o]
    mov AX, [s]
    push AX
    push DI
    call [pt.dword]
  END;
END;

PROCEDURE TestCase.RunTest(name:String; testMethod:Pointer);
BEGIN
  Write('TEST ', name);
  SetUp;
  CallClassPtr(testMethod, self);
  TearDown;
END;
The Prime Factors Kata
Having a simple TPUnit in place, it's time for the kata itself. The seven test methods of PrimeFactorsTest,
PROCEDURE PrimeFactorsTest.Run;
BEGIN
  RunTest('TestOne', @PrimeFactorsTest.TestOne );
  RunTest('TestTwo', @PrimeFactorsTest.TestTwo );
  RunTest('TestThree', @PrimeFactorsTest.TestThree );
  RunTest('TestFour', @PrimeFactorsTest.TestFour );
  RunTest('TestSix', @PrimeFactorsTest.TestSix );
  RunTest('TestEight', @PrimeFactorsTest.TestEight );
  RunTest('TestNine', @PrimeFactorsTest.TestNine );
END;
yield
FUNCTION TPrimeFactors.generate(i:Longint):ArrayListPtr;
VAR factors:ArrayListPtr;
    candidate:Longint;
BEGIN
  factors := new(ArrayListPtr, Init);

  FOR candidate := 2 TO i DO
  BEGIN
    WHILE i MOD candidate = 0 DO
    BEGIN
      factors^.Add(candidate);
      i := i DIV candidate;
    END;
  END;

  generate := factors;
END;
ArrayListPtr is a pointer to a variable sized, user defined list backed by an array of Longints, similar to Java's ArrayList<Integer>. I can't deny I'm a Java guy. Everything I code looks like Java :-) (Probably I would have used a linked list back then instead of a complex object. Something like
TYPE PrimeFactorPtr = ^PrimeFactor;
  PrimeFactor = RECORD
    value:LongInt;
    next:PrimeFactorPtr;
  END;
Still the procedure body looks the same and the kata does not change much.)

(Download full source)

14 January 2010

Prime Factors Kata BASIC

The Prime Factors Kata is a small coding exercise first shown by Uncle Bob in Java several years ago. It has been done in C#, Ruby and probably some other languages. Recently Uncle Bob performed it as katacast in Ruby. (Go watch it! It's really cool. I will wait here.) He was followed by others in C# and Groovy. Anyway I'm sure it has not been done in BASIC. (Since my last post about Scala and BASIC I've felt so "retro".) So here is my Prime Factors Kata done in BASIC V2 (Commodore 64). I couldn't use my Scala BASIC DSL because it's lacking necessary features like GOSUB. So I used the Versatile Commodore Emulator (VICE) instead.

The First Test.
1000 CLR
1010 PRINT "test one ";
1020 i=1
1030 GOSUB 9000
1040 IF pf(0)<>0 THEN PRINT "expected no factors" : STOP
1050 PRINT "green"
Number i is the value to be factored and the array pf is expected to contain the number of prime factors in pf(0) followed by the factors themselves on return of the "generate" function (line 9000).
test one
?UNDEF'D STATEMENT ERROR IN 1030
Add the function.
8990 END
9000 REM ----- function generate
9010 REM in ... i ... number
9020 REM out ... pf() ... factors
9030 RETURN
Run the test.
test one green
The Second Test.
1100 CLR
1110 PRINT "test two ";
1120 i=2
1130 GOSUB 9000
1140 IF pf(0)<>1 THEN PRINT "expected 1 factors:";pf(0) : STOP
1150 IF pf(1)<>2 THEN PRINT "expected factor 2:";pf(1) : STOP
1160 PRINT "green"
test one green
test two expected 1 factors: 0
BREAK IN 1140
9000 REM ----- function generate
9030 IF i=1 THEN RETURN
9040 pf(0)=1
9050 pf(1)=2
9060 RETURN
Unfortunately there is no such thing as "BUnit". So I have to create testing infrastructure along the way. Keeping the tests green helps during the extraction of an "assert" method (line 100).
80 GOTO 1000
90 REM ***** test infrastructure *****
100 REM ----- method assert equals(int)
110 REM in ... me$ ... message
120 REM in ... ex ... expected
130 REM in ... ac ... actual
140 IF ex=ac THEN RETURN
150 PRINT "red"
160 PRINT me$;" expected";ex;" but was";ac
170 STOP
180 RETURN
...
1100 CLR
1110 PRINT "test two ";
1120 i=2
1130 GOSUB 9000
1140 ex=1 : ac=pf(0) : me$="num factors" : GOSUB 100
1150 ex=2 : ac=pf(1) : me$="1st factor" : GOSUB 100
1160 PRINT "green"
The Third Test.
1220 i=3
1230 GOSUB 9000
1240 ex=1 : ac=pf(0) : me$="num factors" : GOSUB 100
1250 ex=3 : ac=pf(1) : me$="1st factor" : GOSUB 100
Run the test.
test one green
test two green
test three red
1st factor expected 3 but was 2
BREAK IN 170
Modify the function.
9050 pf(1)=i
Green again. After the third test it's getting boring. The tests should be refactored to be more DRY:
200 REM ----- method teardown
210 PRINT "green"
220 RETURN
300 REM ----- method setup
310 REM in ... me$ ... test name
320 PRINT "test ";me$;" ";
330 RETURN
400 REM ----- method assert prime factors
410 READ me$
420 GOSUB 300
430 READ i
440 GOSUB 9000
450 READ af
460 ex=af : ac=pf(0) : me$="num factors" : GOSUB 100
470 IF af=0 THEN GOTO 520
480 FOR j=1 TO af
490 READ ex
500 ac=pf(j) : me$=STR$(j)+". factor" : GOSUB 100
510 NEXT
520 GOSUB 200
530 RETURN
990 REM ***** test cases *****
1000 DATA "one", 1, 0
1010 GOSUB 400
1100 DATA "two", 2, 1, 2
1110 GOSUB 400
1200 DATA "three", 3, 1, 3
1210 GOSUB 400
The Fourth Test.
1300 DATA "four", 4, 2, 2, 2
1310 GOSUB 400
9000 REM ----- function generate
9010 REM in ... i ... number
9020 REM out ... pf() ... factors
9025 REM local ... nf ... number factors
9030 nf=0
9040 pf(0)=nf
9050 IF i=1 THEN RETURN
9060 IF INT(i/2)*2=i THEN nf=nf+1 : pf(nf)=2 : i=i/2 : GOTO 9040
9070 nf=nf+1 : pf(nf)=i : i=1 : GOTO 9040
9080 RETURN
Line 9070 is more than needed to get the fourth test green, but it's the first thing that came to my mind.

The Fifth Test.
1400 DATA "five", 6, 2, 2, 3
Works as well, no changes needed.

The Sixth Test.
1500 DATA "six", 8, 3, 2, 2, 2
Again this still works, I "cheated" a bit.

The Seventh Test.
1600 DATA "seven", 9, 2, 3, 3
Now I really need the nested loops.
9000 REM ----- function generate
9010 REM in ... i ... number
9020 REM out ... pf() ... factors
9030 REM mod ... ca ... pf candidate
9040 pf(0)=0 : REM special case
9050 IF i=1 THEN RETURN
9060 IF INT(i/2)*2<>i THEN GOTO 9110
9070 pf(0)=pf(0)+1
9080 pf(pf(0))=2
9090 i=i/2
9100 GOTO 9050
9110 FOR ca=3 TO INT(SQR(i)) STEP 2
9120 IF i=1 THEN RETURN
9130 IF INT(i/ca)*ca<>i THEN GOTO 9180
9140 pf(0)=pf(0)+1
9150 pf(pf(0))=ca
9160 i=i/ca
9170 GOTO 9120
9180 NEXT
9190 RETURN
This includes already two performance optimisations: Handling the special case 2 up front to be able to use STEP 2 and skip every second prime factor candidate. Second using the square root of i as upper bound for the loop. But there is a bug not covered by the tests. Can you spot it? Yet another refactoring to remove duplicate code of adding prime factors to pf.
9040 pf(0)=0 : ca=2 : REM special case
9050 IF i=1 THEN RETURN
9060 IF INT(i/ca)*ca=i THEN GOSUB 9200 : GOTO 9050
9070 FOR ca=3 TO INT(SQR(i)) STEP 2
9080 IF i=1 THEN RETURN
9090 IF INT(i/ca)*ca=i THEN GOSUB 9200 : GOTO 9080
9100 NEXT
9110 RETURN
9200 pf(0)=pf(0)+1
9210 pf(pf(0))=ca
9220 i=i/ca
9230 RETURN
I could still get rid of the duplicate lines 9060 and 9090...

Another Test.
1700 DATA "eight", 10, 2, 2, 5
This reveals the bug in line 9070. If the value contains different prime factors then the last factor is lost. We need another check in line 9110.
9100 NEXT
9110 IF i>1 THEN ca=i : GOSUB 9200
9120 RETURN
END.
One last refactoring to add colours and time measurements to methods "setup" (line 300) and "teardown" (line 200). Two more tests to see the performance of larger (32719) values of i. The function even works for 2^31-1 (huge), but takes quite some time, although the loop is already optimised.Prime Factors Kata tests are all green :-)(Download the source.)

9 January 2010

Scala DSL for BASIC

Sweet holidays - two weeks without work (but unfortunately without any connection to the internet). So what did I do? I did some code katas in Java and Ruby and some reading. Masterminds of Programming is a fine book. After reading the chapter about BASIC, which was the first language I used in the 80ties, I spent some time thinking about the old days of Commodore 64 when GOTO was still state of the art ;-) Winter Holidays

"How about some fun coding or even performing a kata in BASIC?" Without any connection to the internet gwbasic.exe or any other interpreter was out of reach. But BASIC v2 (aka Commodore BASIC) was quite simple. So I started implementing an interpreter myself. The question was if it is possible to implement BASIC v2 as an internal DSL in Scala. DSLs are quite popular in Scala, even for other languages, e.g. Daniel Spiewak's DSL for calling JRuby from Scala. Let's see how far I got...

Hello World
First I want to compile
10 PRINT "Hello World"
RUN
That shouldn't be difficult using implicit conversions.
class Command(val lineNumber:Int, code: =>Unit) {
   def execute = code
}
class NewStatement(line:Int) {
   def PRINT(msg:String) =
      addToMemory(new Command(line) { println(msg) })
}
implicit def Int2NewStmt(line:Int) = new NewStatement(line)

def RUN ...
Note that there has to be a blank after the line numbers. This is not necessary for BASIC. Method addToMemory adds the Command instance containing the code to run (println(msg)) to a list for later execution by the mixed in RUN method. (In case you are not familiar with Scala, the code that finally gets compiled for line number 10 looks like new NewStatement(10).PRINT("Hello World"). Scala is not strict about dots and braces.) By adding PRINT(num:Int) to NewStatement
10 PRINT 12 + 12
20 PRINT 144/12
work as well. Unfortunately I am not able to implement PRINT's concatenating features, i.e.
40 PRINT "a" ; "b"
50 PRINT "a" , "b"
Commodore 64 because ; is the empty statement in Scala and parameter lists containing , need braces.

My old friend where have you gone?
One of the first examples in the Commodore 64 Manual is
10 ? "Commodore 64"
20 GOTO 10
? is just a shortcut for PRINT. So add
class NewStatement(line:Int) {
   ...
   def ?(msg:String) = PRINT(msg)
   def GOTO(target:Int) =
      addToMemory(new Command(line) { lineCounter = target })
}
The implementation of RUN uses lineCounter to keep track of executing line numbers. When the Command with GOTO's code is executed, it changes the number of the next line to be run. Tada! Now I can GOTO Scala ;-)

Assignment and Evaluation
Let's tackle some basic assignments.
10 X% = 15
20 X = 23.5
30 X$ = "SOME STRING"
40 PRINT X
50 PRINT X$
Using the same approach as before (with some refactoring of addToMemory()):
class NewStatement(line:Int) {
   ...
   def X_=(d:Double) = {
      addToMemory(line) { variables.X = d }
   }
   def X:Double = throw new Error()

   def X$_=(s:String) = {
      addToMemory(line) { variables.X$ = s }
   }
   def X$:String = throw new Error()
}
The empty getters are necessary for Scala to recognise the methods as overridden bean properties and are never called. Although % is a valid Scala method name, it's not possible to suffix method names with it. So integer "variables" like def X%_=(i:Int) can't be defined and line 10 will never compile. (One could escape `X%`, but then the BASIC code would have to look like 10 `X%` = 15.)

For evaluation I need a method without parameters called X, in the scope of the current code, like RUN. The method must not return the value of the variable because that would be too early. It should return code that yields the value of the variable when called (yes that's a function ;-).
def X:Function0[Double] = new Function0[Double] {
   override def apply = variables.X
}
def X$:Function0[String] = new Function0[String] {
   override def apply = variables.X$
}
The container variables holds the values of all variables during "BASIC runtime", i.e. when RUN is called. To use the variables in PRINT, it must accept these functions as arguments as well.
class NewStatement(line:Int) {
   ...
   def PRINT(msg:Function0[String]) = {
      addToMemory(line) { println(msg()) }
   }
}
Basic Instinct Variables
To allow arbitrary variables all variable names have to be predefined for assignment and for evaluation. That's impossible. But fortunately early basic used only two letters for variables. [A-Z] and [A-Z][A-Z0-9] yield 962 valid names for each Double and String variables. So all these getters and setters could be generated. The generated trait VariableAssignment with all setters is mixed into NewStatement and the generated trait VariableEvaluation with all getters is mixed into the main code. (Unfortunately the Scala compiler crashes with StackOverflowError when mixing in the trait with its nearly 4000 methods.) The container variables is better replaced with two maps, one for Double and one for String variables.

Express Yourself
To add two variables, i.e. to add the values of two variable evaluations, I change all literals and Function0s to expressions (read custom function instances). This is a major refactoring but Scala makes it easy for me. Literals are still covered by implicit conversions.
abstract class DoubleExpression {
   def value:Double

   def +(d:DoubleExpression) =
      new DoubleExpression {
         override def value = DoubleExpression.this.value + d.value
      }
   def *(d:DoubleExpression) =
      new DoubleExpression {
         override def value = DoubleExpression.this.value * d.value
      }
}
class LiteralDoubleExpression(val value:Double) extends DoubleExpression

implicit def Double2DoubleExpression(value:Double) =
   new LiteralDoubleExpression(value)
implicit def Int2DoubleExpression(value:Int) =
   new LiteralDoubleExpression(value.toDouble)

trait VariableAssignment {

   def assign(name:String, d:DoubleExpression) = {
      addToMemory(line) { memory.set(name, d.value) }
   }
   def unused = throw new Error("never used")

   def X_=(d:DoubleExpression) = assign("X", d)
   def X:DoubleExpression = unused
   ...
}
class NewStatement(line:Int) extends VariableAssignment {
   ...
   def PRINT(msg:StringExpression) =
      addToMemory(line) { println(msg.value) }
}
trait VariableEvaluation {

   class VariableDoubleExpression(val fromVariable:String)
      extends DoubleExpression {
      override def value = memory.get(fromVariable)
   }
   def X = new VariableDoubleExpression("X")
   ...
}
The code above shows the changes needed for Double. Similar classes and methods are needed to cover StringExpressions. Now let's examine what happens for 10 X = X + 1: The method call X of trait VariableEvaluation returns a VariableDoubleExpression (which will yield the value of "X" when called) on which + is invoked with the implicitly converted new LiteralDoubleExpression(1.0). The resulting DoubleExpression is the argument for the setter method X_= of trait VariableAssignment in NewStatement.

Let it Flow (Control)
Till now the Scala BASIC lacks any flow control, e.g. a conditional GOTO like
40 IF X < 5 THEN 20
class NewStatement(line:Int) {
   ...
   def IF(expr: => Boolean) = {
      class ConsumeThen(expr: => Boolean) {
         def THEN(target:Int) = addToMemory {
            if (expr) ... // goto target
         }
      }
      new ConsumeThen(expr)
   }
}
Method IF consumes the boolean expression (as a function) and returns some instance of a class that only allows the following THEN. Method THEN in turn adds the code to execute the GOTO to the list of Commands. BASIC comparison operators are different from Scala's and I have to implement them in DoubleExpression:
abstract class DoubleExpression {
   ...
   def <(d:DoubleExpression) = value < d.value
   def <>(d:DoubleExpression) = value != d.value
}
The implementation of FOR is a bit more complex mainly because the BASIC "runtime" has to keep track of the current loop, so NEXT statements are executed proper.
10 FOR X = 1 TO 5
20 PRINT "COMMODORE 64"
30 NEXT X
To allow 1.TO(5) I define a LoopRangeExpression that handles the boundaries of the loop:
class LoopRangeExpression(val lower:DoubleExpression) {
   var upper:DoubleExpression = null
   def TO(u:DoubleExpression) = {
      upper = u
      this
   }
}
implicit def Int2LoopRangeExpression(value:Int) =
   new LoopRangeExpression(new LiteralDoubleExpression(value.toDouble))
Scala compiles the loop statement into new NewStatement(10).FOR().(X()) = new LoopRangeExpression(new LiteralDoubleExpression(1.0)).TO(new LiteralDoubleExpression(5.0)). That's why method TO has to return its instance, to match the update method (used for setting array elements). So FOR has to return an object that defines update(variable: VariableDoubleExpression, range: LoopRangeExpression). Inside method update all necessary code to run the loop is put into the BASIC runtime part. Both FOR and NEXT use DoubleExpression to capture the variable names but never evaluate these expressions.
class NewStatement(line:Int) {
   ...
   def FOR = {
      new Object() {
        def update(variable:VariableDoubleExpression,
                   range:LoopRangeExpression) = {
           val usedVariable = variable.fromVariable
           addToMemory {
              ... // set usedVariable to range.lower.value
           }
           addLoop(usedVariable) {
              ... // increment and check condition
           }
        }
      }
   }
   def NEXT(d:VariableDoubleExpression) = addToMemory {
      ... // perform next for name d.fromVariable
   }
}
Functions
Support for functions like INT() or LEN$() is simple to add. I use another trait BasicFunctions, that defines the functions in terms of expressions, e.g.
def INT(d:DoubleExpression) =
   new DoubleExpression {
      override def value = d.value.floor
   }
Time for Refactoring
Although I factored out the traits of getters, setters and functions there is still too much stuff in the main class. Parsing BASIC should be separated from the logic that stores and executes the lines of code, the "runtime". I call this logic the BasicMemory.
Scala DSL for BASIC class hierarchyTrait BasicParser is the main parser, which mixes in VariableEvaluation and BasicFunctions. It contains the NewStatement and all implicit conversions. Trait BasicMemory contains the program code as list of Commands and the logic needed to run them. The parser communicates with the runtime by using a small interface LinesOfCode for adding commands, runtime and memory (read variable) operations. BasicApplication is just for convenience, so BASIC programs may be written as
object Demo extends org.codecop.basic.BasicApplication {

   10 PRINT "Hello World"
   RUN
}
The Whole Beast
The shown code was developed with Scala 2.7.7. Download the full source here. There is more implemented than covered in this post, e.g. INPUT, END or STEP. This little DSL has been an experiment. It's far from complete. It lacks important features like DATA/READ or GOSUB/RETURN. I reckon that supporting DIM or DEFFN would be difficult as well. And there are things that can't be done in Scala, e.g. the shown problem with %. Still if this is interesting to people, I may do a proper release into an OSS project somewhere later.

23 July 2008

DOS Commandline Tools

A Bit of History
I always liked coding, right from my very first line of Basic back in 1986. Later I did some 65xx-Assembler work and in 1991 I finally went for Turbo Pascal. During the nineties I created a lot of stuff for MS-DOS based systems either during university projects or just for fun. Being a lazy programmer, I produced a lot of tools to perform repetitive tasks more easily. DOS and Windows never shipped with many utilities, unlike other operation systems. (Today there are Windows ports of more powerful tools available.)

Recently, when reorganising all my historic code I recovered these command-liners. Well, good old Turbo Pascal is definitely out-dated and most executables wouldn't even run any more. The reason is that the CRT unit as supplied by Borland has a few defects causing RTE200 on fast computers or at least inconsistent delay problems. Fortunately there are CRT Replacement Units for Turbo & Borland Pascal Compilers which fix all these problems. So I spent some time recompiling all my 280k lines of Pascal code. Now they work on newer computers as well. The tools are divided into several groups: Data-File-Hacking, File-Utilities, Image-, Sound/Music- and Text-Manipulation.

Rusty Ring Most of the tools provided here were created in 1994 to 1998. They were developed for 16 bit MS-DOS based systems and do not support long file names or recently introduced file formats. Most likely some of them are superseded. Use them at your own risk! Almost all tools show their usage when called with parameter "-?" or "/?". Unfortunately the help messages are in German... Some of them are highly experimental. and some are probably totally useless. I say it again, use them at your own risk!

The Data-File-Hacking package contains programs for disassembling, extracting data from files and memory, extracting files from special data files of some games, hex- and eor/xor-representation of binary data and extensive search and compare procedures. (Download hacking tools).
  • ????HACK contains code to "hack" i.e. unpack or uncompress data files of certain games.
  • BIN2HEX converts binary data into hex table text file.
  • BIT2BYTE expands every bit of binary data to a full byte.
  • BUB2ASM converts Bubble-Asm format into TASM-Asm (prefix hex numbers with 0).
  • BYTE2BIT compress every byte with value != 0 to a set bit, else not set bit.
  • CHANGBYT changes a single byte in a file.
  • CUTBIN removes the first bytes of a binary file.
  • CUTBYTES removes bytes of a binary file starting at a certain offset.
  • CUTNOP removes all lines from a text file that start with 90<space> (NOP command).
  • CV2ASMDB converts CodeView output into ASM "DB" format.
  • CV2INLIN converts CodeView output into Turbo Pascal inline assembler format.
  • EOV exclusive-or viewer to find byte sequences of 'protected' files.
  • EXTRACT extracts parts of files from larger files.
  • HEX converts hex, decimal, octal and binary numbers into each other.
  • HEX2BIN converts a sequence of hex number into a binary file.
  • HEXTSR (TSR) shows hex conversion window on key stroke ALT-S.
  • MEMLOAD loads binary data into memory without executing it.
  • MEMSAVE saves memory contents to a file.
  • MEMSVCNV saves contents of conventional memory to a file.
  • MEMSVXMS saves contents of free XMS memory to a file.
  • MEMTSR (TSR) saves memory contents to a file on key stroke ALT-S.
  • RECH calculates arithmetic expressions.
  • SAVESCRN saves current contents of video text memory to a file.
  • SCRNTSR (TSR) saves current contents of video text memory to a file on key stroke ALT-S.
  • VOCEXTR finds and extracts Creative Labs Voice (VOC), Wave (WAV) and XMidi (XMI) from larger files.
  • WADIT edits and displays contents of ID software's WAD files (DOOM data files).
  • WAVEXTR finds and extracts RIFF Wave and IFF files from larger files.
  • XADD adds/xors all bytes of a file to a checksum.
  • XCHGBYTE replaces or swaps specific values in binary files.
  • XCOMP extended version of DOS COMP.
  • XSEARCH searches several files simultaneously.
  • XSEQUENZ searches results of XCOMP runs for ascending/descending sequences.
  • XWHAT prints the value of a certain position in files.
  • XWHAT2 prints all the value of a certain position in files.

File-Manipulation features tools for comparing, renaming, setting time and date, wildcard operations and hardware detection. (Download file tools.)
  • BRAKE! slows down CPU using busy waits in interrupt 8 (INT08).
  • CHKFILES reads files from disk to check their integrity.
  • DETECT ultimate hardware detection and benchmark tool.
  • DFOR executes a DOS command for all file names in a list.
  • DUPLICAT calls COPY to duplicate directory trees, copies only new files.
  • FILLDISK calculates somehow optimal distribution of files when saved to floppy disks.
  • GETTIME measures the time another DOS command needs to execute.
  • ISWIN checks for currently running Windows 3.1 or 95.
  • LENCOMP compares files similar to COMP but based on length, ignoring path and name.
  • NOTHING Do Nothing Loop.
  • RENALL renames a wildcard file set according to a name list.
  • SCOMP compares files similar to COMP and deletes identical ones.
  • SFOR executes a DOS command for a wildcard file set.
  • TDIR lists files similar to DIR with their date time to be used for TTDAT.
  • TIMETOUC (or short TT) changes the date and time of a file.
  • TO searches for a directory and changes current working directory to there.
  • TTDAT executes TIMETOUC for a list file produced with TDIR.

Field Working Tools
The Image-Manipulation collection provides help for viewing header information of images, viewing special picture formats, viewing special animation formats, palette manipulation, Windows BMP generation and font designing and using. (Download image tools.) Some of the tools need a VESA compatible driver to display images in SVGA graphics mode.
  • ANIM creates an rotating animation from a Bitmap (BMP).
  • ANM plays 320x200x256 sized ANM animations (with LPF ANIM magic bytes).
  • APAL2BIN converts ASCII RGB-palette into binary.
  • BGI2DOOM converts Turbo Pascal's BGI images into Doom 1.2 textures.
  • BMP2ASC converts 640x400x256 sized BMP into an ASCII image.
  • BMP2BGI converts uncompressed BMP into Turbo Pascal's BGI format.
  • BMP2DRW converts 80x50x16 sized BMP into my own DRW format for display in text mode.
  • BMP2PAL saves colour palette of a BMP.
  • BMP2PLAN converts 640x480x16 sized BMP into 4 colour planes.
  • BMP2RAW converts 320x200x256 sized BMP into raw-format.
  • BMP2TXT converts BMP into ASCII image depending on brightness values.
  • BPAL2ASC converts binary RGB-palette (768 bytes) into decimal text format as needed for RAW2GIF.
  • COLTABL displays table of DOS colours.
  • DRAWPIC editor for drawing 80x25 DRW image files.
  • DRAWTXT editor for drawing ASCII images.
  • FNTMAGIC editor for drawing 16x8/8x8 fonts.
  • FONTHACK displays binary data as 16x8/8x8 fonts.
  • FONTVIEW displays an F16 font.
  • GIF2BGI converts a GIF into one or more BGI images.
  • GIF2SPR converts 160x160 sized GIF into 64 BGI images useable as sprites in games.
  • GIFHEAD displays header information of GIF images.
  • HLS2RGB converts ASCII HLS-palette into ASCII RGB-palette.
  • KPACK compresses in my own R9 format, similar to RLE8 with slightly better compression.
  • LINESET set number of lines text mode (25, 28, 33 or 50).
  • PACK compresses files with RLE algorithm.
  • PACKFL4 compress 4-bit animations with distinct frames, my own FL4 format.
  • PAL2BMPP converts binary palette into BMP palette with empty padding.
  • PLAY plays 320x200x256 sized Autodesk FLI and 640x480x256 sized FLC.
  • PLAYFL4 plays 640x480x16 sized FL4 animation.
  • PLAYFLI plays 320x200 sized Autodesk FLI and FLC animation.
  • RAW2BMP converts RAW images into BMP.
  • RGB2HLS converts ASCII RGB-palette into ASCII HLS-palette.
  • SETBORD sets border colour in console.
  • SETFONT sets an F16 font in console.
  • SHOWBMP displays unpacked 1024x768x256 sized BMP.
  • SHOWC64 displays Commodore 64 Koala Painter and other image formats.
  • SHOWGIF loads a few GIFs into XMS memory, displays and cycles them (mini slide show).
  • SHOWIT displays GIFs and slide shows of GIFs using external VPIC.EXE.
  • SHOWPAL displays colours of a palette.
  • SHOWPIC displays arbitrary data as colour values of an image.
  • SHOWRAW displays arbitrary data as 16 or 256 colour values of an image up to 1024x768x256.
  • SHOWSVGA (or short EV) displays arbitrary data as colour values of an image up to 1280x1024x256.
  • SHOWVGA displays arbitrary data as VGA image (320x200x256), part of EOV.
  • TXT2BMP converts text into 80 times lines sized BMP.
  • TXT2RAW converts text into 80 times lines sized raw image data.
  • UNPCKFL4 unpacks and displays 4-bit animations (FL4 files).
  • UNRLE8 unpacks RLE8 compressed files.
  • XCOLOR changes specific colour values of a BMP in image data, not in palette.
  • XTRC_BMP extracts sub-image out of an uncompressed BMP into a new BMP.

The Sound/Music-Manipulation tools support conversion of special sound formats into common formats and between different types of formats, playing of VOCs, WAVs and XMidis via Soundblaster and PC-Speaker and getting header information of audio files. (Download sound tools.) To use your audio hardware you need a DOS styled driver emulator like VDMSound.
  • AMIG2SND converts Amiga signed raw sound data (SND) into PC format.
  • DWD2SND converts DiamondWare digitised data into SND format.
  • MAC2SND converts Apple Mac sound data (SND) into SND format.
  • MANYINST lists instruments of an XMidi found in the *.ad file.
  • MIDHEAD displays header information and event list of Midi files (MID).
  • PAWE plays XMidi (XMI) using AWE32 hardware.
  • PCM2VOC converts Space Hulk PCM into VOC.
  • PCM2WAV converts AquaNox's PCM into WAV.
  • PCMF plays Creative Music File (CMF), needs SBFM to be started before.
  • PCMF17 plays CMF.
  • PMID plays XMidi using Midi hardware.
  • PMPU plays XMidi using MT32MPU hardware.
  • PSOUND plays sound files like VOC and WAV using PC speaker hardware.
  • PVOC plays VOC.
  • PWAV plays WAV.
  • PXMI plays XMidi using Soundblaster hardware.
  • RAW16TO8 converts raw 16 bit samples into 8 bit by ignoring every second byte.
  • RAW2SGN converts signed into unsigned samples by adding 080h.
  • RAW2SND converts raw samples into SND format.
  • SAM2WAV converts Modplayer Sample data (SAM) into WAV.
  • SND2WAV converts SND into WAV.
  • SYN2WAV split SYN data files into separate WAVs.
  • VAC2WAV converts VAC, RAC and MAC (ST-TNG CD, 11025Hz) into WAV.
  • VCMF plays all CMFs in the current directory, needs SBFM to be started before.
  • VCMF17 plays all CMFs in the current directory.
  • VMF2WAV converts Creatix sound 4800Hz (VMF) into WAV.
  • VMPU plays all XMidis in the current directory using MT32MPU hardware.
  • VOCHEAD displays header information of VOC files.
  • VVOC plays all VOCs in the current directory.
  • VWAV plays all WAVs in the current directory.
  • VXMI plays all XMidis in the current directory.
  • WAV16TO8 converts 16 bit WAVs into 8 bit.
  • WAV2SAM converts WAV into Modplayer Sample data (SAM).
  • WAV2SYN combines several WAVs into a SYN.
  • WAV2VMF converts WAV into Creatix VMF.
  • WAV2VOC converts WAV into VOC.
  • WAVHEAD displays header information of WAV files.
  • WAVMIX mixes WAVs using different algorithms.
  • WAVMORPH morphs a WAV into another (very experimental).
  • WAVST2MO converts 8 bit stereo WAV into mono format.
  • XMIHEAD displays header information of XMidi files.
  • XMITRACK extracts single tracks from a multi-track XMidi.

Abandoned to nature
Text-Manipulation (ASCII) offers methods for converting, replacing, sorting and different kinds of removing of pieces of text in text files. (Download text tools.)
  • ASCTABL displays table of ASCII characters.
  • ASCTABL2 displays table of ASCII characters together with their decimal values.
  • BIN2ASC removes all bytes 0-31 from a file to edit it as text.
  • C64TOASC converts Commodore 64 text into ASCII format.
  • COPYLINE copies all lines of a text file which contain a search term.
  • CUT10 removes the first 10 columns in each line (for DIS86).
  • CUTLAST removes the last given characters in each line.
  • CUTLEAD removes the first given characters in each line.
  • CUTLINES removes every some lines some more lines, e.g. remove two lines every ten lines.
  • CUTPOS removes all text after a given column position.
  • CUTSPACE removes trailing spaces.
  • D2E replaces all 'D' with 'E', used to convert FORTRAN output into gnuplot data.
  • DELNEWS removes all lines of a text file which start with search term.
  • DELSPACE removes all double blanks.
  • DIR2HTML creates an HTML index file containing the current folder entries.
  • DIR2TEX creates a TeX index file containing the current folder entries.
  • DOC2TXT converts WinWord umlauts back into DOS characters.
  • DOWN converts all letters into lowercase.
  • EXP2DEZ converts scientific numbers (e.g. 1.3E-1) into decimal numbers (e.g. 0.13).
  • FIT2PIX reduces data points to one value per pixel, used for gnuplot and TeX.
  • FORM2TXT converts HTML form data into plain text.
  • GERADEN solves a linear equation.
  • GNUSUB subtracts values from the second column, used for translation of gnuplot data.
  • INSP2TEX converts INSPEC search results to LaTeX bibitems.
  • LINECNT counts the lines of a text file.
  • LINESORT sorts like SORT but blocks of lines instead of single lines.
  • LINESUB replaces whole lines of a text file, configured by line number.
  • MERGLINE merges two text files line wise (today's editors know this as 'column mode').
  • MOVE_TEX moves TeX elements inside EmTeX pictures.
  • NOSPACE removes all single blanks.
  • ROUND rounds all decimal numbers.
  • SEQ2TXT converts Commodore 64 sequential file data (SEQ) into PC char-set.
  • SKALIER scales numbers by a factor, used for gnuplot data.
  • SLIM_TEX removes all unnecessary horizontal \put(lineto), used for gnuplot and TeX.
  • STRIP_% removes all comments (%) from TeX source.
  • STRIP_C removes all comments from FORTRAN 77 source.
  • TASTCODE prints the key code of the pressed key.
  • TEST7BIT checks if a text only contains ASCII characters > 127
  • TEXTDEL removes every some lines some more lines, e.g. remove two lines every ten lines.
  • TEXTINS inserts a text file into another at a certain position.
  • TEXTSUB replaces characters in a text file.
  • TURNBACK save a file in reverse order, i.e. last byte first.
  • TURNLINE turn all lines backwards, i.e. from right to left.
  • UMBRUCH joins all lines and adds new line breaks at given position.
  • UP converts all letters into uppercase.
  • UP11 converts all letters and umlauts into uppercase.