Usually an informal description of a programming language—such as that found in a textbook-- is sufficient for most programmers.
A more precise description is needed for developing tools such as compilers, debuggers, and profilers. Such a description is also useful for formal proofs of program correctness.
A formal language specification has two parts: a formal description of the language's syntax and a formal description of its semantics.
Syntax refers to the tree-like structure of a valid program. Syntax is usually specified using an EBNF grammar.
Semantics refers to the runtime behavior of a (syntactically valid) program.
There are three approaches to formal specification of semantics: axiomatic, denotational, and operational. (Denotational and axiomatic semantics are more advanced and won't be discussed here.)
Operational semantics is specified by a reference interpreter. The heart of a reference interpreter is the execute function:
Value execute(Expression exp, Environment env) { ... }
It would be circular to implement the reference interpreter in the language being specified. Instead, the reference interpreter is implemented in a language already understood by the intended audience. (Or one that is dead-simple such as assembly language for some simple virtual machine, or lambda calculus.) We refer to this implementation language as the meta-language and the language being specified as the object language. (Philosophy trolls might point out that if the meta-language is not understood by the audience then it will need to be specified in some meta-meta-language, leading to a potential infinite regress!)
For each type of object language expression, the execute function describes how it will be executed by the meta-language interpreter. For example:
execute(if (exp1) exp2 else exp3, env) = if (execute(exp1, env)) execute(exp2, env) else execute(exp3, env)
In this example red indicates object language syntax, while black indicates meta-language syntax. It tells us that if/else in the object language more or less translates to if/else in the meta-language. Not a big surprise, but our philosophy trolls might point out that the designers of the object language are free to define the semantics of if/else in any weird way they desire.
The goal of a reference interpreter is transparency, not efficiency. Using a reference interpreter as a guide, a programmer might build a more efficient interpreter.