A programming language is not designed merely by choosing keywords. Its grammar decides which programs can be formed, its type and scope rules decide which are meaningful, and its runtime rules decide what a valid program does. One small let expression shows all three at work: it becomes a token stream, then an abstract syntax tree, then a static type of Int, then the value 19. Change the scope rule, the evaluation strategy, the parameter mode or the mutation model, and programs of the same shape return 15 instead of 25, or leave (3,8) where you expected (8,3).
Language design: the decisions behind a programming language
Language design fixes syntax, static and dynamic semantics, plus a runtime model. Syntax governs form; static semantics checks names and types; dynamic semantics defines values and state changes.
Design axis | One possible choice |
|---|---|
Surface syntax | Braces or indentation |
Name binding and scope | Lexical or dynamic scope |
Type system | Explicit annotations or inference |
Evaluation and control | Strict or non-strict evaluation |
Data and memory | Mutable references or immutable values |
Error model | Exceptions or result values |
Abstraction | Functions, modules or classes |
Designers balance readability, implementation effort, safety, performance, portability and compatibility. A language may reject 3 + 2.5 without conversion, or promote 3 to 3.0 and return 5.5; its specification must choose.
From characters to grammar and abstract syntax
This grammar binds multiplication tighter than addition, keeps comparison looser than both, and leaves let and if outermost:
Expr -> "let" id ":" Type "=" Expr "in" Expr
| "if" Expr "then" Expr "else" Expr
| Compare
Compare -> Add [ ">" Add ]
Add -> Mul { "+" Mul }
Mul -> Primary { "*" Primary }
Primary -> integer | "true" | "false" | id | "(" Expr ")"
Type -> "int" | "bool"Our source is let x: int = 4 in let y: int = x * 3 in let x: int = 7 in if y > 10 then x + y else y + 5. Tokens include keyword let, identifier x, punctuation :, type keyword int, operator =, integer literal 4 and keyword in. Whitespace separates them but never enters the AST.
Its exact tree is Let(x:Int, Int(4), Let(y:Int, Mul(Var(x), Int(3)), Let(x:Int, Int(7), If(Gt(Var(y), Int(10)), Add(Var(x), Var(y)), Add(Var(y), Int(5)))))). Without punctuation, precedence is explicit: x * 3 is one Mul subtree, y > 10 is the condition, and the final Add nodes are its branches. Parsing in Compiler Design, Top-Down and Bottom-Up develops this step.

Worked example: type-check and evaluate the expression
Start with Gamma0 = {}. Since 4:Int, check the first body under Gamma1 = {x:Int}. Since x * 3:Int, use Gamma2 = {x_outer:Int, y:Int}. Inner x:Int shadows outer x but not y:Int.
Here y > 10:Bool, while both branches are Int, so the if and complete program are Int. Changing only the else branch to false:Bool causes a branch-type mismatch and rejection, even though these values select the then branch.
Under lexical scope, bind outer x = 4; calculate y = 4 * 3 = 12; bind inner x = 7; calculate 12 > 10 = true; evaluate only 7 + 12 = 19. Outer x created y, while inner x supplied the branch. Syntax-Directed Translation and Code Optimization shows how attributes or tree walks attach types and intermediate results to nodes. Type checking and optimisation remain different passes.
Names, binding and scope: why one call gives 15 or 25
A declaration introduces a name; binding connects it to an entity. Scope is where it is visible; lifetime is when the entity exists. Inner x = 7 hides outer x = 4 only in its body; y stays 12.
let x = 10
fun addX(y) = x + y
fun run() = let x = 20 in addX(5)
run()Lexical scope resolves free x where addX was defined, giving 10 + 5 = 15. Dynamic scope searches callers, so run supplies x = 20, giving 20 + 5 = 25. A closure pairs code with the lexical environment required by free names. Lexical follows source nesting; dynamic follows active calls. Lexical does not mean global.

Type-system design: checking, coercion and abstraction
Checking time and conversion policy are separate. Static checking verifies before evaluation; dynamic checking may defer operand checks. Independently, 3 + 2.5 may produce 5.5 by promotion or require conversion. “Strong typing” settles neither rule.
Inference removes annotations, not rules. let count = 4 can infer count:Int, while fun twice(n:Int):Int = n + n keeps an explicit boundary; twice(6) returns 12. For identity<T>(x:T):T, T=Int makes identity(7) return integer 7, and T=Bool makes identity(true) return boolean true. Overloading selects separate implementations; generics, overloading and subtyping remain distinct.
Runtime design: evaluation, parameters, mutation and errors
If first(a,b) returns a, strict evaluation of first(42, 10 / 0) raises division by zero. Non-strict evaluation returns 42 because b is unused. false && ((10 / 0) > 1) avoids division only when && short-circuits.
For x = 3, y = 8 and swap(a,b) { temp=a; a=b; b=temp; }, call by value swaps copies, leaving (3,8). Call by reference aliases caller variables, producing (8,3). Passing an object reference by value is not call by reference.
With a = [2,4]; b = a; b[0] = 9, shared references make both names observe [9,4]. With value copies, a stays [2,4] and b becomes [9,4]; immutability requires a new value. Syntax alone does not reveal the model.
An error model is explicit too. Result values can return Ok(5) for divide(20,4) and Err(DivideByZero) for divide(20,0); exceptions transfer control. Safety depends on whether errors are visible and handled.
GATE-style and interview questions on language design
Written papers ask you to identify tokens, read an AST, check types, trace bindings, compare evaluation orders, calculate caller values or follow aliases. Interviews ask the same content as a why: why run() returns 15 and not 25, or why swapping through a reference changes the caller while swapping a copy leaves it untouched.
Prompt | Answer | Governing rule |
|---|---|---|
Central expression type |
| Both |
Central expression value |
| Lexical scope and selected then branch |
Else branch changed to | Rejected | Static branch-type mismatch |
|
| Free name uses definition environment |
|
| Free name follows active callers |
Call-by-reference |
| Parameters alias caller variables |
Do not confuse tokens with AST nodes, grammar ambiguity with semantic ambiguity, scope with lifetime, or static typing with annotation. Lexical binding does not follow callers. Short-circuiting is an operator rule. Reference by value is not pass by reference. More drills in exactly these forms sit under GATE CS Exam Preparation.
Language design in short and the next step
Recall characters -> tokens -> AST -> static checks -> runtime evaluation, then inspect scope, types, evaluation order and mutation. Here outer x=4, y=12, inner x=7, condition true and result 19.
With y > 15, the condition is false and y + 5 = 12 + 5 = 17. Remove inner let x: int = 7 in but keep threshold 10, and the then branch uses outer x = 4, giving 4 + 12 = 16.
For the wider GATE sequence around this topic, work through GATE Guidance by Sanchit Sir. To put scope, parameter passing and mutation into code you can run and break yourself, the C Language course is the shorter route.




