Two different questions
When you look at a line of code, you can ask two separate questions. The first is whether the text is well-formed: does it follow the rules about which symbols may appear and in what order? That is syntax. The second is what the program does when it runs: what values it produces, what it prints, what it changes. That is semantics.
In everyday languages the two are tangled together, because we learn them at the same time. A curly brace, a keyword and an operator all look like part of “the language.” Odd languages are valuable because they pull the two apart so that you can see each one by itself.
An analogy
Think of a board game. The syntax is the set of pieces and the board: what they look like and how they are laid out. The semantics is the rules: what a move does and how the game is won. You could replace the chess pieces with bottle caps without changing the game, as long as everyone agrees which cap stands for which piece. The look changed; the meaning did not.
Programming languages work the same way, and some esoteric languages are essentially this experiment carried out on purpose.
Same meaning, different notation
The clearest demonstration is the Brainfuck reskins. Ook is a well-known example: instead of single punctuation characters, its programs are made of the words “Ook” followed by a period, question mark or exclamation mark, in pairs. Each pair stands for one of the original eight operations. A program in this form looks like a strange chant, but the underlying behavior is the same as the language it copies.
This tells you something useful. Two languages can have completely different syntax and identical semantics. If you wrote a program that mechanically replaced each Ook pair with the matching symbol, you would get a program that does exactly the same thing. That kind of mechanical mapping is a simple case of language translation, and it works because only the surface changes.
Syntax that disappears
Some languages push the surface to an extreme. The Whitespace language gives meaning only to spaces, tabs and line breaks and treats everything else as commentary. A valid program can look like an empty page, with its real content invisible. The semantics are those of a stack-based language with ordinary arithmetic and flow control, but the syntax hides it.
A reader expecting meaning to be visible is baffled, yet an interpreter has no difficulty. Nothing about the computation is strange; only the way it is written down.
Syntax in more than one dimension
Ordinary source code is a line of text read from left to right and top to bottom. The Befunge language breaks that assumption. A program is a grid, and which instruction runs next depends on the direction the pointer is currently moving. Two-dimensional languages in general make the layout itself part of the meaning: where a symbol sits relative to its neighbors matters.
Some languages go further and let shape and color carry meaning. In Piet colors determine the operation, based on the change in color from one block to the next, rather than what a character looks like. A Piet program can resemble a painting. ASCII art programs, by contrast, can be arranged so that the text looks like a picture while remaining a runnable program, using layout as decoration as well as instruction.
Mirror commands, as in Fish and similar languages, treat some characters as surfaces that redirect the pointer. The syntax is a set of slashes and angle brackets that look like part of a diagram, and the semantics is a path.
Same syntax, different meaning
The reverse can happen too. The same characters may mean different things in different languages, or even in different modes of one language. Many languages that use single characters as commands reuse familiar-looking symbols, but a plus sign or a period does not always add or print. Reading code without knowing the semantics can mislead you, because familiar symbols suggest a meaning that may not apply.
A related case is a language with a mode that changes how characters are read. In Befunge, a quotation mark switches the interpreter into a mode where characters are pushed onto the stack as values instead of being executed as commands. The same letter is an instruction in one mode and data in another. The grammar stays the same; the meaning of a character depends on state.
Why the separation matters
Keeping the two ideas apart helps in several practical ways:
- Building tools. An interpreter has two jobs: recognize well-formed programs and carry out their meaning. These can be designed independently, as described in building interpreters, where the loading stage handles the syntax and the execution loop handles the semantics.
- Evaluating languages. When comparing two languages, it helps to ask whether they differ in notation only or in what programs can do.
- Learning. A learner who confuses unfamiliar syntax with difficult semantics may give up on a language whose ideas are actually simple.
Some languages have almost no syntax
At one extreme are languages where nearly any character is a valid piece of a program. In a grid language where unrecognized characters are simply ignored or reflected, there is hardly any way to write a syntax error. The entire difficulty lives in the semantics: working out where the pointer goes and what the stack holds.
At the other extreme, a language with elaborate grammar rules may reject many programs before they ever run, so most of the challenge is syntactic.
Reading odd code
When you meet an unfamiliar esoteric program, a useful habit is to separate the two questions deliberately. First, work out the notation: which symbols are commands, which are data, which are ignored. Then trace the behavior, one step at a time. Dividing the work this way makes unfamiliar languages much less intimidating, since you are only ever solving one problem at a time.