XML

kent logo

CO538 Anonymous Questions and Answers

This page lists the various questions and answers. To submit a question, use the anonymous questions page. You may find the keyword index and/or top-level index useful for locating past questions and answers.

We have taken the liberty of making some minor typographical corrections to some of the questions as originally put. Although most of the questions here will have been submitted anonymously, this page also serves to answer some questions of general interest to those on the course.

When asking questions, please do not simply paste in your code and ask what is wrong with it, as these types of question are hard to answer in a public forum (resulting in either no answer, or an answer which will only be of use to the person asking the question). In many cases, a question about code can be formulated in general terms. For example, if you have an `IF' block that results in an `incorrect indentation' error, you might ask, ``what is the correct syntax for an occam `if' statement ?''. Questions of this form are easier to answer, and are meaningful to all.

Questions that are not suitable for these public pages (i.e. those that contain assignment-specific code), should be mailed to your seminar-leader.


Question 21:

Submission reference: IN839

My suspend process works but is giving some very strange output. The code is as follows:

  ...  code deleted

It stops the program just fine but will not allow me to resume. To try and test it a little bit i changed the last line from "SKIP" to "out ! x", just to see what was pending on the output channel. This allows the program to resume on the next keypress. However: in every case (despite how long I wait before resuming), the next number output is 43 and the other output from the program is not what I'm expeciting. For example:

  1	 1	  4
  2	 3	  49   <-- pause here
  43	 46	  135
  43	 89	  181  <-- resume key
  3	 92	  188

Do you have any idea why this may be happening? Or more to the point, is my suspend process along the right lines?

Answer 21:

Sorry - your code fragment is deleted for the same reason as in the previous question.

However, your suspend process polls its control channel and, once it suspends operations, that's all it does. It spins forever, never releasing the processor and, so, your keyboard monitor process never gets back in to allow you to stop it! [Actually, that looks like a bug in the Transterpreter, which should re-schedule after a failed poll attempt. We will look into that.]

But that doesn't let you off the hook. That poll here is not needed ... and, if you have to poll, you shouldn't just spin ... without engaging (communicating) with some other process ... or having a (small) timeout ... in the polling loop. Then, everything gets scheduled again!

Putting a communication in your polling loop is what you do in the "test it a little bit" you describe. However, you output an unitialised variable and that accounts for the mysterious "43" you see. Uninitialised variables have no default value - the compiler warns you if it sees you using them. [I suspect our Transterpreter people decided it would be amusing to set them to 42, :), which they are absoultely entitled to do ...]

Keywords: q4


Question 22:

Submission reference: IN842

Is is ok if my 's' keypress doesn't use blackholes? It doesn't seem to cause any problems not using it, but I just wanted to make sure it was ok as you mentioned it during the class

Answer 22:

Yes. If I mentioned it in class, it wasn't to do with this assignment. Sorry - I can't remember doing that. There is no need for "black holes" in this assessment.

Keywords: q4


Question 23:

Submission reference: IN852

Is there anyway to use the numbers, integrate and pairs processes in a SEQ? For SEQ, I can't figure out how to make them work, but they are fine for a PAR.

But I can't figure out how to stop them in a PAR, I would use an IF statement if it was a SEQ; but I'm really stuck and I spent all day trying to make it work ...

Answer 23:

No, there is now way to use the numbers, integrate and pairs processes in a SEQ. The code:

  SEQ
    numbers (a!)
    integrate (a?, b!)
    pairs (b?, c!)
    ...  etc.

first runs the numbers process and waits for that to finish ... which it never does ... so the integrate never runs ... which means there is no process taking the output from numbers ... which means that numbers never even manages to output its first number!

So, trying to run these processes in sequence ... that is a pretty fundamental error.

By "stopping them", I assume you mean blocking them so that the output stream is frozen. To do this, no change at all to those processes is required! You just have to put something in their output stream - an obstacle ("pause") process that you can switch between allowing traffic through or blocking it. To block traffic, simply don't input anything and, therefore, don't forward anything.

If by "stopping" you mean the last modification (response to a 'q' keypress), just terminate the obstacle process. Now, there's an "air-gap" in the wiring and no data can flow and pressing further keys cannot repair that! To see deadlock immediately, you should also terminate the keyboard monitor process - otherwise, that's still alive waiting for keyboard input.

Keywords: q4


Question 24:

Submission reference: IN854

Why does my code just print:

  0
  1
  2
  3

then deadlocks? If I change the printstream call to connect to the f channel (rather than c), then it just prints:

  0

then deadlocks? If I change it to connect g, then it just deadlocks before printing! Here's my code:

  PROC starty (CHAN BYTE keyboard?, screen!, error!)

    CHAN INT a, b, c, d, e, f, g:
    CHAN INT x:
    INT xx:
    [3]CHAN INT q:

    WHILE TRUE

      PAR

        numbers (a!)
        delta (a?, b!, c!)
        integrate (b?, d!)
        delta (d?, e!, f!)
        pairs (e?, g!)

        print.stream (100000, c?, screen!)
  :

Answer 24:

You never use your channel array q, nor your channel x nor your variable xx.

The WHILE TRUE betrays a bad misunderstanding! Its loop body is a parallel network of processes - none of which ever finish. So, that network never finishes ... so the loop body never finishes ... and your loop never loops!! Just get rid of it.

In your code, numbers outputs its first zero ... delta forwards it to integrate and print.stream ... print.stream prints it (first output line) ... integrate adds it to zero and sends zero to the second delta ... which forwards it to pairs and channel f. Channel f has no receiving process, so that's the end of the second delta which can't continue until it completes the output on f.

Meanwhile, pairs takes in the zero and waits for its second input. This is on its way ... as numbers can now output its second number (1) ... the first delta forwards that to integrate and print.stream (which prints it - the second output line). Meanwhile, integrate adds it to its running sum and outputs that to the second delta ... which, sadly, is blocked still trying to output ... which means that integrate is now blocked.

Meanwhile, numbers can now output its third number (3) ... etc.

Eventually, the first delta gets jammed trying to output to integrate ... so numbers gets jammed trying to output to the first delta. Because, integrate and pairs have internal concurrency, they can actually take in at least one more input than outlined above (because they have parallel input and output processes). This is why you get a couple more lines of output before complete deadlock is establised, :(.

If you connect print.stream to the g channel, the earlier delta processes jam straight away as there is no process taking their second output channels. Everything jams very quickly ... though the first zero should get through to be printed? Probably, the Transterpreter bails out with its deadlock message before Windows gets around to showing the zero in the terminal window.

Your mistake is using print.stream, which has only a single input channel. You should be using print.streams, whose code is given in your starter file and which takes an array of input channels (one from each delta and one from the final pairs).

Keywords: q4


Question 25:

Submission reference: IN851

Hi - I have just upgraded to vista and I have installed the Transterpreter and all works fine and all behavior is normal. However when I run the program the output from the program does not appear in the terminal window.

I have asked a friend to check the file on another machine and the code works fine. I have just tried installing the latest version Transterpreter on a XP machines and the same problem occurs. Any ideas.

Answer 25:

This is from the Transterpreter team:

"One thing to try is to make sure to resize the occPlug window. There is currently a bug where the terminal window will not appear at all unless the occPlug window itself is big enough to contain it. I am working on fixing this, along with a number of other issues.

"Other than that we have not ever tried the Transterpreter in Vista nor are we likely to have access to any Vista machines soon(?). Perhaps the student can get in touch with us directly if he's interested in helping with testing and ironing out futher Vista bugs..."

Keywords: transterpreter

Referrers: Question 26 (2006)


Question 26:

Submission reference: IN858

Ref. to Question 25 (2006), I've been running it in Vista for a while and it all works 100% so long as you're running the latest Java release.

Answer 26:

:)

Keywords: transterpreter


Question 27:

Submission reference: IN861

Hi I need some help! I spent ages trying to get this to work, but it doesnt seem to want to. I'm still stuck on the reset numbers to zero, so I have some questions!! [Stuff omitted.]

[Editor: the above question was posted at 14:00:02 on the day of the submission deadline ... the following was at 14:25:44.]

Hi, I know this is cutting it a bit fine but I've been stuck for days with question 3. When I run the program, the 'i' and 'n' keys work as expected however, when I press 'p', the program output doesnt change and the keyboard is unresponsive. [Stuff omitted.]

Answer 27:

Sorry - that was cutting it too fine! We were attending a Board of Studies meeting from 2:00-4:30pm that afternoon ... and 4:30 was after the deadline. We try to turn around answers as quickly as possible - but cannot promise to do so within the day.

The story has a happy ending for at least one of the questioners ... who followed up an hour later with:

"Forget the last question, i figured it out!! Finally!"

:)

Keywords: q4


Question 28:

Submission reference: IN871

Is there any chance we can get our raptor quota increased? I couldnt get 128MB free on my raptor account for the VM we needed to run the robotics occam, the lowest I could get it down to without deleting files from other courses was 90MB and our limit is 200MB.

Answer 28:

Done! The raptor filespace limit for all students taking Co631 has been increased to 350MB.

Keywords: robodeb , transterpreter


Question 29:

Submission reference: IN872

Sorry if theres a real obvious anwser to this question that I'm missing but is there a way for me to save(/load) the occ files a create in RoboDeb to my Windows drive? (for the sake of organisation and keeping backups)

If the directory structure for RoboDeb stored somewhere on my Windows install?

If this isn't possible I can always FTP or email my files so not too much of a problem but is it safe to assume that anything save when using the VMWare will be secure and wont dissappear for next time I use RoboDeb? Thanks.

Answer 29:

The VMWare VM is "safe" to store your files on in one sense, and "unsafe" in others.

The VM saves its data where you installed it to. For most of you (all of you?) this was in your Raptor filespace. So, there is a file that represents "your" part of the VM virtual disk. It contains your data. So, as long as you don't delete *that*, you can launch the VM and work with your files. If that file is on Raptor, at least it is backed up.

Now, you're right in thinking that this is not exactly the best place to put your work. There are a few ways you can get things on/off the VM; you'll have to decide what works best for you.

Hopefully, that answers the question. If you have more questions, you can either post them here, or you can join the tvm-discuss mailing list:

    https://ssl.untyped.com/cgi/mailman/listinfo/tvm-discuss

This is a good place to ask questions and get answers that pertain to the Transterpreter itself or the RoboDeb VM. The whole TVM team is on that list, and we're glad to help make the tools better in any way that we can.

Keywords: robodeb , transterpreter

Referrers: Question 35 (2006)


Question 30:

Submission reference: IN904

Over the past few weeks I have gained a useful insight into concurrency and generally enjoyed the content of the module. However as the course progresses its seems that the content of the module has started to slope more towards learning the Occam semantics rather than generic concurrency issues.

You explained clearly why we are using Occam to teach these issues but I feel for the course to become useful to students you need to relate/compare these issues against a more widely adopted language/style.

You frequently mention Java's problem with its concurrency off topic; but I feel that if you spent some time demonstrating these problems this would benefit us greatly. At the precise moment I don't feel that I can apply much of what has been taught to more common language platforms as not much as been related to traditional approaches like Java threads, this view is held by the majority of students I have spoken to. For example I would be interested to know if languages such as .NET, Python, C etc are more suitable than others for building systems with a high level of concurrency.

Does the module have any intentions of relating the lessons learned from Occam back to more widely adopted approaches?

Please don't take these comments negatively as I believe overall the course is delivered very well. :-)

Answer 30:

Thank you very much for your question - you have put a lot of care and thought into it and I'll try to do the same in this answer!

There are several quite different concurrency models currently being used. By far the most common is threads-and-locks: in this, multiple threads share a single common memory space - some of which they use privately and some for communicating with each other. This is supported by standard libraries for C and C++ (e.g. Posix - offering spinlocks, mutexes, semaphores) and Java monitors (synchronized, wait, notify and notifyAll). The primitives make no statement as to when and how to use them ... but they are easy to learn. However, they are notoriously difficult to reason about and, hence, to use correctly. Too much locking leads to deadlock; too little locking leads to system corruption and, eventual, failure. The circumstances leading to either of these conditions are usually unrepeatable ... which makes debugging near impossible. Almost all systems programmed at this level are wrong. Because all systems above trivial levels of complexity have to deal with concurrency and are programmed at this level, almost all systems are wrong. This causes loss of lots of money and loss of life. For comments, see:

  http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html

and search for the words: "extreme caution"!

Threads-and-locks was the first (and is still the main) approach to concurrency. Other approaches had to be found to improve the state of the art. We are studying one based upon the idea of communicating processes, which only operate on private memory and use a range of special mechanisms for communication and synchronisation (e.g. channels and barriers). Examples of this include Microsoft's Singularity:

  http://research.microsoft.com/os/singularity/

which founds its security/simplicity on occam-pi ideas ("Software Isolated Processes", though at a much coarser granularity and with their own language derived from C#). There is also the Actor concept:

  http://en.wikipedia.org/wiki/Actor_model

which is trivial to apply from occam-pi. And there is occam-pi itself (and JCSP), based on the formal process algebras of CSP and the pi-calculus.

But there are still other models - such as Software Transactional Memory:

  http://en.wikipedia.org/wiki/Software_transactional_memory

derived from ideas in databases and recently favoured by the functional programming community (e.g. with Haskell). Another contender comes from the Join Calculus and is exemplified by Microsoft's C-omega language (prevously called "Polyphonic C#"):

  http://research.microsoft.com/comega/
  http://research.microsoft.com/~nick/polyphony/intro.htm

So, there are lots of models out there and there will be more to come. We're teaching something we believe is the simplest and most powerful available right now. We don't want to teach you stuff we know doesn't work, even though that's what most of the world uses. Having taught you the good stuff and given you the chance to mature and appreciate working with it, you will get a shock when you try the bad stuff, :(.

If you browse around the materials in the course folder on raptor:

  \courses\co631\etc\README.txt

you will find a tutorial on the standard Java concurrency (threads-and-locks) model ("Java Threads in the Light of occam/CSP"):

  \courses\co631\etc\java-occam-csp.pdf

This presents the basic mechanisms, but points out real difficulties in their use. The second half of this paper (Section 3) shows how to drop the occam/CSP model on top of all that bad stuff. It assumes you have some knowledge of the occam-pi model of processes and channels - which you do! The implementation of channels is rather tricky (all that bad stuff has to be used) - but once done, you don't need to bother with it again and work at the occam model level, :). Eventually, that work led to the JCSP package libraries for Java (which I'll try to leave time in the module to present). Similar libraries for C (CCSP) and C++ (C++CSP) have also been made.

I hope this has helped put things in better context. Mastering the good ideas gives us a much better chance of surviving the bad ones, simply because we know that we have to be so very careful (and because we have a good model that shows us - technically - how to take that care).


Question 31:

Submission reference: IN945

This is probably an issue with my icky code but it is worth checking. KRoC produces:

    Fatal-occ21-q7.occ(162)- internal inconsistency found in routine "chan_direction_of -- not channel"
    
    *********************************************************************
    * The compiler has detected an internal inconsistency.		    *
    * Please email kroc-bugs@ukc.ac.uk with a copy of the code which    *
    * broke the compiler, as this is probably a compiler bug.	    *
    * Please include the KRoC version and any other relevant details.   *
    *********************************************************************

The offending code fragment is:

  INITIAL MOBILE []CHAN BOOL down IS MOBILE [num.phils]CHAN BOOL:

So, what's wrong? Am I doing something silly or did I find a bug in KRoC? (I highly suspect the former rather than the latter!). I've not yet tried in the Transterpreter, I guess this would be a good plan...

Answer 31:

I'd expect the Transterpreter to fail in the same way &mdash it uses the same compiler. The problem is a compiler bug, of sorts. Dynamic mobile arrays of channels are not entirely well supported, as you've discovered. The issue is that the compiler isn't allowing you to refer to the whole array 'down' (when you attempt to pass it as a parameter to another procedure).

Unfortunately I'm way too busy at the moment to fix compiler bugs (until June/July at least), so you'll have to find another way to code what you want.. Sorry :-(. But logically, what you're trying to do is sane.

PHW writes: all you're trying to do (I think?) is declare an array of channels with 'num.phils' elements. In the above, you've adapted a line from 'print.streams' that declares an array whose size is computed at run-time (from the size of the channel array argument it was passed). Your adaptation is logical but, as Fred says, the compiler does not yet support all aspects of dynamic array allocation – such as for channels.

However, if 'num.phils' is a compiler-known value (not a variable set at run-time) – e.g. it is declared somewhere towards the top of your file by:

    VAL INT n.phils IS 5:

then you can use it to declare a fixed-size array of anything, including channels, trivially – for example:

    [num.phils]CHAN BOOL down:

That should be all you need? :)

Keywords: coursework , q7


Question 32:

Submission reference: IN984

Why do you have comments like this in code?:

  --{{{  think
  ...  code
  --}}}

isn't it "--" for comments?

Answer 32:

The dash-pair is for the comment. The triplets of curly braces are for folding. You'll need an editor that understands folding to make any use of these, but vim/gvim (common) and origami (old) do, and I suspect there are others. It basically allows the editor to collapse whole chunks of code into single highlighted lines, making working with any sizeable bit of code a whole lot easier. People who haven't discovered folding are at a disadvantage in my opinion :-). Some editors also understand how to do folding based on language constructs, rather than explicit markup as is commonly used.

Keywords: folding


Question 33:

Submission reference: IN995

How do you create an array of strings? This is the closest I have got:

  VAL [][]BYTE name.phil IS ["Bob", "James", "Fred", "Jane", "Amy"]:

But it doesn't work, and theres not a whole lot of information online :( Any hints?

Answer 33:

See Question 104 (2003) in particular, and other questions relating to strings and string-handling. Lots of stuff on-line!

Keywords: strings , string-handling

Referrers: Question 58 (2007)


Question 34:

Submission reference: IN987

In the older answers, you mention model answers. Are these still available to look at?

Answer 34:

OK - I've put model answers to completing q1.occ, q2.occ, q3.occ and q4.occ in the raptor folder:

    \courses\co631\answers\

Keywords: q1 , q2 , q3 , q4


Question 35:

Submission reference: IN997

I can't transfer text from the VMware to windows, even after reading the comments for the other question (Question 29 (2006))about this.

Where can I find "subversion"? I can't find it anywhere, and I searched all through my home directory on raptor, and there don't seem to be any folders holding any files that I save! It's driving me crazy!!!!

I just wanted to copy and paste some code to ask you a question, but even that's not working. My code (painstakingly copied by hand) is something like this:

    -- proc that takes 1 chan int and returns 2 chan ints

    PROC chanDouble(CHAN INT in1?, in2?, out!)
      INT x1, x2, x3:
      CHAN INT cx:
      WHILE TRUE
        SEQ
          in1 ? x1
          in2 ? x2
          x3 := x1 + x2
          cx ! x3
          out ! cx
    :

It gives the error:

    I/O list item 1 does not match protocol

and the line of the error is the "out ! cx". It's such a simple piece of code. Why isnt it working? And how can i use or find "subversion"?

Answer 35:

Possibly the simplest way to move files between your VMWare linux environment and anywhere else is to use the scp command. A Google search ("SCP manual") just found the following pages that should help you do this:

    http://amath.colorado.edu/computing/software/man/scp.html
    http://vanha.cc.utu.fi/english/diskspace/scpmanual.html

Subversion, which gives you a source control system, can be downloaded from:

    http://subversion.tigris.org/

but I think I'd find scp quicker to understand and use.

The comment above your PROC is a little misleading. Your PROC takes three INT carrying channels and doesn't return anything – once invoked, it never returns ... because its WHILE TRUE loop never terminates! Nevertheless, it can do useful work by accepting stuff on its input channels and generating stuff on its output channel.

Your internal declaration of CHAN INT cx: looks very wrong. If we declare an internal channel, it's because the implementation is going to go PARallel, with one sub-process being given the writing (output) end of the internal channel and another getting the reading (input) end. That doesn't happen in your code: in its loop cycle, it inputs two numbers from its two input channels, adds them together and then tries to output them down the internal channel (cx ! x3). At that point, it would get stuck since there is no process running in parallel and inputting from that internal channel.

However, your code doesn't compile because the line, out ! cx, says: "output the channel cx down channel out". But we can only send integers down channel out – not channels! Hence, the compiler's complaint.

Maybe you meant to output the result of the addition down (the external) channel out? If so, all you need is:

          out ! x3

Keywords: robodeb


Question 36:

Submission reference: IN988

I am probably being dense, but is there a way to get code like this to work:

    PAR
      left ! TRUE
        out ! got.left
      right ! TRUE
        out ! got.right

or:

    PAR
      SEQ
        left ! TRUE
        out ! got.left
      SEQ
        right ! TRUE
        out ! got.right

Where out is a channel to the PROC that animates everything. I currently get an error about parallel output to out if I put SEQs in, but is there a way to do it?

Answer 36:

Your first code fragment is syntactically malformed – indentation errors. Your second has a legal syntactic structure but has a semantic error – it tries to have two processes outputting in parallel to the same channel (out).

The simplest solution is not to bother reporting to the animation process when a philosopher manages to pick up an individual fork! The fork processes will be doing that – as they are picked up (or put down) – and the animation process will receive those reports. So, having the philosophers do that as well is redundant.

Only if you weren't equiping the forks with report channels would it be necessary for the philosophers to report fork movements. There are two ways to do that. Either give each philosopher two report channels: one through which it reports everything bar, say, left fork movements ... and one exclusive for reporting those latter. Or run a short-lived multiplexing process in parallel with the parallel hand movements (to pick up or put down forks), with two channels for reporting between the hand movements and the multiplexor. The multiplexor waits for just two reports, sending them to the external report channel and, then, terminates (along with the two hand movements).

I'd go for the simple solution!

Keywords: q7


Question 37:

Submission reference: IN996

Is there an easy way to pass Strings between processes? It seems that you have to specify the length, such as [30]BYTE, but this doesn't seem a particularly amazing way to do it. Is there a way to say [however-long-it-needs-to-be]BYTE?

Answer 37:

If this question is prompted for animating the dining philosophers, relax – you don't have to pass strings around. The college components (philosphers, forks and security guard) should send coded messages to the animating process, which reacts by outputting text strings to the screen. The coded messages can be simple BYTEs (or INTs) representing states like "I'm hungry", "I'm on the table" or "I've counted in n philosophers".

However, if you have some other reason for passing around strings, use a counted array protocol as presented in the last lecture. For example:

    PROTOCOL STRING IS BYTE::[]BYTE :

This protocol (actually message structure) allows you to pass around arrays of BYTEs (e.g. strings), preceded with a (BYTE) count of how long they are. Since the largest byte value is 255, that is the longest string that can be passed with this protocol. This means that, so long as a receiving array for such a message has 255 elements, it will always be large enough – i.e. there will be no run-time array bounds overflow.

Keywords: q7 , protocol


Question 38:

Submission reference: IN1002

I'm presuming BYTEs are a bit more efficient than INTs for passing small numbers around the PROCs and am therefore using them. Assuming they are better, can I change the INTs in security to BYTEs, so I don't get all confused?

Answer 38:

A BYTE communication shifts one byte – an INT communication shifts four bytes. So, yes, it is more efficient to shift only one when there is no need to shift four. But, in this application, you would need a pretty sharp sense of time to notice!

The proper reason is an engineering one: if you can do the job with less work, do so! This is a variety of Ockham's Razor: don't invent stuff that you don't need – doing so opens unnecessary scope for error. It was precisely by breaking this rule that the Arianne 5 rocket crashed in 1996, fortunately on its first test flight (though not so fortunately for the four fly-formation telescopes hitching a ride).

The security guard only has 5 numbers to report – either 0, 1, 2, 3 or 4 philosophers have been counted through to the dining room. These are all in the range of BYTE values. So, yes, let the security guard count and report in BYTEs – not INTs.

Keywords: q7


Question 39:

Submission reference: IN1003

Using something like this:

    PAR i = 0 FOR 5

Can I make it produce Strings like "Fork 1", "Fork 2" etc., with some sort of concatenation? Such as "Fork " + 1 in Java?

Answer 39:

No. What on earth might a PAR replicator have to do with that?

Strings are not a primitive type in occam-pi. There are byte arrays that we use for them, but there are no built-in (or library) operators – such as concatenation. The follow-on module, Stage 3 Co632, introduces mobile and dynamically sized and allocated arrays. These, rather than the fixed-sized arrays used in this module, are from what strings (and a string library) should be made.

Meanwhile, please be assured that there is no need for any string concatenation when animating the dining philosophers. Only your animation process should deal with strings – outputting them to a screen channel. To output the string "Fork 2", where 2 is the value of a variable (say i):

    SEQ
      out.string ("Fork ", 0, screen!)
      out.int (i, 0, screen!)

Keywords: strings


Question 40:

Submission reference: IN1004

Not a question really, but &ndash keeps appearing in your answers :S I presume you mean - ?

Answer 40:

My mistake! I'd missed the semi-colon off the HTML symbol:

    & n d a s h ;

spelt out above with spaces – so you can see it! Your browser should render this as a medium-sized dash (a bit longer than a plain text "-").

My browser (Firefox 1.5.0.7) was forgiving of the missing semi-colons and rendered it anyway – so I didn't see my mistake. It should all be corrected now. Many thanks.

Valid CSS!

Valid XHTML 1.0!

This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.
Last modified Mon May 20 13:50:23 2013
This document is maintained by Fred Barnes, to whom any comments and corrections should be addressed.