Language Design in Programming Languages: Syntax, Types, Scope and a Worked Evaluation

Trace one expression from source text to a value, then compare the design rules that make scope, typing, evaluation and mutation behave differently.

KnowledgeGate Team

Exam prep & CS education

Updated 31 Jul 20266 min read

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.

A language-processing pipeline turning the let expression into a token stream, an AST, a static type of Int, and the evaluated result 19.

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.

Lexical versus dynamic scope for the call run(): free x resolves to the defining x=10 giving 15, or the caller x=20 giving 25.

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

Int

Both if branches are Int

Central expression value

19

Lexical scope and selected then branch

Else branch changed to false

Rejected

Static branch-type mismatch

run() under lexical scope

15

Free name uses definition environment

run() under dynamic scope

25

Free name follows active callers

Call-by-reference swap(3,8)

(8,3)

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.