No history yet

Language Specification

Blueprint for a Language

Every programming language, from C to Python, follows a strict set of rules. These rules aren't just suggestions; they are the language's DNA, defining what is valid and what is gibberish. Before you can write a compiler, you must first become an architect and design this blueprint. This formal blueprint is called a language specification.

The specification dictates the language's syntax (how it looks) and its semantics (how it behaves). For syntax, we use a formal grammar to define the structure of every valid program. This isn't a loose description; it's a precise, mathematical definition that a machine can understand. It tells the compiler how to recognize statements, expressions, and all the other building blocks of your code.

Defining the Grammar

To define a language's grammar, we use a notation called (BNF). BNF is a way of describing the rules, or productions, that generate all possible valid strings in a language. Each rule breaks a high-level construct down into its smaller constituent parts. Think of it like a recipe: a program is made of statements, a statement might be an assignment, and so on, until you reach the basic ingredients, like numbers and identifiers.

Let's look at a simplified grammar for arithmetic expressions using Extended BNF (EBNF), which adds some convenient shorthand:

expression    ::= term ( ( "+" | "-" ) term )*
term          ::= factor ( ( "*" | "/" ) factor )*
factor        ::= number | identifier | "(" expression ")"
number        ::= digit+
digit         ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"

Here’s how to read this:

  • ::= means "is defined as."
  • | means "or."
  • () group items together.
  • * means "zero or more times."
  • + (in digit+) means "one or more times."

This grammar states that an expression is a term followed by zero or more + or - operations. A term is a factor followed by zero or more * or / operations. This elegantly enforces the standard order of operations. A factor can be a number, a variable name (identifier), or another expression in parentheses.

Core Design Decisions

Beyond syntax, you must decide how your language behaves. Two of the most critical decisions are the type system and scoping rules.

The type system governs how the language handles data types like integers, strings, and custom objects. The main choice is between static and dynamic typing.

  • Static Typing: Types are checked at compile-time. Languages like C and Rust use this. It catches type-related bugs early and can improve performance because the compiler knows the size and layout of data. The tradeoff is less flexibility; you often need to be more explicit about types.

  • Dynamic Typing: Types are checked at run-time. Python and JavaScript are popular examples. This allows for more flexible code, as variables can hold different types of data at different times. The downside is that type errors won't be found until the program is running, which can lead to unexpected crashes.

Next are the scoping and binding rules, which determine where a variable can be accessed. Most modern languages, including C, use (or static scope). This means a variable's visibility is determined by its location in the source code. If a variable is defined inside a function, it can only be accessed within that function and any functions nested inside it. The structure of your code blocks defines the scope.

The alternative, dynamic scope, resolves variables by searching up the call stack. It's rare in modern languages because it can make code harder to reason about, as a function's behavior can change depending on who calls it.

Finally, you map out the specific features. Will your language have pointers like C? First-class functions like JavaScript? A garbage collector? Each decision adds a new set of rules to your specification and new work for your compiler. This specification becomes the definitive guide for your language, ensuring a logical and consistent foundation for everything that comes next.