Programming Languages

Programming Language is a formal language designed to describe tasks that humans want computers to perform and to express those tasks in a form that computers c...

Programming Language is a formal language designed to describe tasks that humans want computers to perform and to express those tasks in a form that computers can understand and execute. Programming languages are designed to represent a wide range of computational models, including algorithms, data structures, control flow, abstraction, and parallel processing, making them one of the fundamental technologies underlying software development. Modern programming languages are used across nearly every area of computing, including system software, web applications, mobile applications, artificial intelligence, embedded systems, game development, and scientific computing. Each language has distinct characteristics in terms of syntax, execution model, memory management, type system, performance, and developer productivity, reflecting its design goals and philosophy. Programming languages are generally executed by being translated into machine code or an intermediate representation through a compiler or interpreter. In computer science, they have been extensively studied and continuously developed from various perspectives, including language design, syntax, semantics, type systems, compilers, runtimes, and standard libraries.

History

The history of programming languages did not begin suddenly with the advent of electronic computers. Its origins can be traced to early attempts to express computational procedures using a consistent set of symbols and rules, enabling machines to perform those procedures automatically. Over time, programming languages evolved from simple notations for instructing machines into formal systems capable of expressing algorithms, data, abstraction, state transitions, logical relationships, and computational models.

Mechanical Calculation and the Emergence of the Programming Concept

In the early 19th century, Joseph Marie Jacquard developed the Jacquard loom, which controlled weaving patterns through the arrangement of holes punched into cards. Although the punched cards of the Jacquard loom did not contain instructions for numerical computation, they demonstrated that a machine's behavior could be altered according to patterns recorded on an external medium. This concept is widely regarded as an important precedent that later influenced program-controlled machines.

Beginning in the 1830s, Charles Babbage designed the Analytical Engine, a general-purpose mechanical computing machine. The Analytical Engine was conceived with a processing unit called the *Mill* for performing calculations, a memory unit called the *Store* for holding numerical values, input and output mechanisms, and punched cards that carried both operational instructions and data. Although the machine was never completed, its separation of processing and memory, together with its ability to execute different calculations based on externally supplied instructions, closely resembled the architecture of modern general-purpose computers.

In 1843, Ada Lovelace translated an article describing the Analytical Engine written by Luigi Menabrea into English and added a series of notes that were longer than the original text itself. Among these, *Note G* contained a detailed table describing the sequence of operations, variable movements, intermediate results, and iterative processes required to compute the Bernoulli numbers using the Analytical Engine. This table is generally recognized as the first detailed program ever published for a machine. However, since Charles Babbage had also documented computational procedures for the Analytical Engine around 1837, it is more historically accurate to regard Lovelace not unconditionally as the first programmer, but as the first person to systematically publish and explain a machine-oriented algorithm.

Lovelace also argued that the Analytical Engine could process not only numerical calculations but any subject that could be expressed according to formal rules. This represented an early view of programs as general procedures for manipulating symbols rather than merely producing numerical results. Because the Analytical Engine itself was never completed, however, these programs could not be executed on an actual machine, nor could they evolve into a practical programming system during that period.

The Mathematical Foundation of Computability

During the 1930s, before electronic computers became widely available, researchers sought to define computation and algorithms in mathematical terms. Alonzo Church developed the Lambda calculus, a formal system for function abstraction and application, while Alan Turing introduced the Turing machine in his 1936 paper, describing a computational model based on symbols recorded on an infinite tape and a finite set of state transitions.

Turing further defined the Universal Turing machine, a machine capable of reading the description of another Turing machine and simulating its behavior rather than performing only a single predefined computation. This concept became the theoretical foundation of the general-purpose computer, in which both instructions and data are stored in the same memory and different tasks are performed simply by replacing the stored program. Although lambda calculus and the Turing machine described computation in different ways, they were later proven to be equivalent in the class of computable functions they could express. These computational models went on to have a lasting influence on programming languages, including function invocation, recursion, evaluation rules, type theory, and formal semantics.

Early Electronic Computers and Machine Language

The earliest electronic computers of the 1940s were very different from the modern practice of writing and executing source code. Some systems implemented computational procedures by physically reconnecting cables, changing switches, or rewiring circuits. Modifying a program often required reconfiguring the hardware itself. As the stored-program concept gradually became widespread, instructions began to be stored as numerical values in memory or on punched tape, allowing programs to become increasingly independent of the physical hardware.

Early programs were written directly in Machine code, the numeric instruction set executed directly by the central processing unit. Programmers had to manually specify operation codes, memory addresses, and branch targets as numerical values. Since different computer architectures used different instruction sets, programs often had to be rewritten almost entirely for another machine. Even a small error in an address calculation or a single incorrectly entered instruction could cause an entire program to malfunction, making programs extremely difficult to read, understand, and maintain.

To reduce these difficulties, Assembly language was introduced, assigning symbolic names such as ADD, LOAD, and JMP to machine instructions. An assembler translated these symbolic instructions into machine code while calculating memory addresses automatically. Although assembly language was significantly more readable than raw machine code, it remained tightly coupled to the instruction set architecture of a specific processor, and programmers still had to design program logic around the machine's registers and memory organization.

The First Programming Languages

What qualifies as the first programming language depends on how a programming language is defined.

  • Ada Lovelace’s computational procedure for the Analytical Engine is often cited as the first detailed algorithm published for a specific machine.
  • Plankalkül is often cited as the first high-level programming language to be designed.
  • Short Code is often cited as one of the earliest high-level notation systems used on an actual electronic computer.
  • The A-0 System is often cited as an early automatic programming and compilation system.
  • FORTRAN is often cited as the first practical high-level compiled language to achieve widespread use.

Konrad Zuse designed Plankalkül between 1942 and 1945. Plankalkül was designed to represent not only numerical values but also arrays, Boolean values, record-like compound data, conditional statements, and iterative structures. It is regarded as the first high-level programming language design because it attempted to express problem-solving procedures at a higher level rather than merely listing the instructions of a specific machine. However, because of wartime and postwar conditions, it was not implemented on Zuse’s computers at the time, and its details only became widely known later.

Short Code was one of the earliest high-level languages, proposed around 1949 and used during the early 1950s. It allowed mathematical expressions to be written symbolically instead of using machine instructions. Rather than generating a complete machine-code program through a compiler in the modern sense, however, its instructions were interpreted and executed by an interpreter. It was easier to write than machine code, but its execution was slow and its expressive capabilities were limited.

The A-0 System, developed by Grace Hopper between 1951 and 1952, was an automatic programming system that allowed programmers to specify predefined subroutines symbolically, after which the system located the required machine-code routines and combined them into a program. At the time, it was called a compiler and is widely described as the first compiler. In modern classifications, however, it also resembled a linker or loader that arranged and connected subroutines more than a compiler that translated an entire high-level language. Nevertheless, it became an important step in the development of later compilers by demonstrating that parts of the programming process could be automated by the machine itself.

The 1950s: The Practical Adoption of High-Level Languages and Compilers

During the 1950s, high-level programming languages began to become practical. These languages allowed programmers to describe programs in forms closer to the mathematical or business structure of a problem than to the instruction set of a machine, while translators converted the resulting programs into machine code. During this period, major language families emerged in response to differing needs in scientific computing, symbolic processing, and business data processing.

FORTRAN

In 1954, an IBM research team led by John Backus began developing FORTRAN—short for Formula Translation—for the IBM 704. For high-level languages to gain broad acceptance at the time, they had to produce execution performance close to that of machine code written by hand. As a result, the design of an optimizing compiler was considered just as important as the syntax of the language itself during the development of FORTRAN.

Commercially released in 1957, FORTRAN allowed scientific and engineering calculations to be described using mathematical expressions, variables, loops, and conditional branches. Programmers no longer needed to manage register allocation and detailed machine instructions manually, while the compiler analyzed written expressions and generated efficient machine code. FORTRAN was not the first high-level language itself, but it is regarded as the first high-level compiled language to be widely adopted in practical industrial and research environments. The success of FORTRAN demonstrated that high-level languages and compilers could be practical without sacrificing a substantial amount of performance.

The official name of the language later began to be written as Fortran. Fortran continued to be revised, adopting array operations, structured control statements, modules, user-defined data types, object-oriented features, and parallel-processing capabilities. Despite being one of the earliest programming languages, it remains in use in modern high-performance computing, numerical analysis, weather simulation, and physical simulation.

FLOW-MATIC and COBOL

Unlike scientific computing, business tasks such as payroll, accounting, inventory management, and administration placed greater importance on reading and classifying large numbers of records and generating reports than on evaluating complex mathematical formulas. Grace Hopper argued that business users should be able to use commands resembling English words rather than mathematical notation.

FLOW-MATIC, developed by Hopper’s team, was an early language that expressed data-processing procedures using English-like words such as INPUT, OUTPUT, and COMPARE. FLOW-MATIC demonstrated the potential of business data-processing languages distinct from mathematically oriented languages and influenced efforts to create a common business language that could be used on computers from different manufacturers.

In 1959, the United States government, several computer manufacturers, and user organizations formed CODASYL to develop COBOL. The COBOL 60 specification was published in 1960. COBOL adopted long keywords resembling English sentences, explicit data divisions, and record-oriented file-processing structures. It was intended as a business language independent of the machine instructions of any particular manufacturer and was later used extensively in core information systems for finance, insurance, government, and large enterprises. COBOL also helped establish the importance of language specifications and compiler conformance to standards.

LISP

In the late 1950s, John McCarthy developed LISP, a language intended for artificial intelligence and symbolic computation. Unlike FORTRAN, which centered on numerical arrays, LISP treated symbols and lists as fundamental forms of data. It emphasized concepts such as function definitions, recursion, conditional expressions, dynamic data structures, and automatic memory reclamation, while also allowing program code itself to be represented as list-based data.

The LISP paper published in 1960 systematically described methods for processing recursive functions and symbolic expressions on computers. LISP’s function-centered model of computation was influenced by lambda calculus and had a long-term impact on functional programming languages, macro systems, automatic memory management, interactive development environments, and artificial intelligence research.

ALGOL

Unlike FORTRAN, which originated in a specific IBM environment, researchers in Europe and the United States sought to create an algorithmic notation language independent of both machines and national boundaries. This effort produced ALGOL 58 in 1958, followed by the official report for the revised ALGOL 60 in 1960.

ALGOL 60 provided block structures delimited by begin and end, local variables whose scope was limited to a block, recursive procedures, and systematic control structures. The syntax of the language was described using a formal grammar in the family of Backus–Naur Form, which became a standard method for programming-language specifications and parser design. Although ALGOL itself saw less industrial use than FORTRAN or COBOL, its syntax and structure influenced many languages, including Pascal, C (programming language), and Simula.

The 1960s: The Emergence of Structured Programming, Education, and Object Concepts

During the 1960s, the use of computers expanded beyond research institutions and military facilities into businesses and universities. As programs grew larger, the challenge was no longer merely to create a notation that replaced machine code. It became increasingly important to structure programs so that they could be understood, modified, and developed collaboratively by multiple people.

The block structures and local variables introduced by ALGOL-family languages provided a foundation for separating parts of a program into independent units. The Structured programming movement, which sought to reduce unrestricted branch instructions and organize programs around sequential execution, selection, iteration, and procedure calls, also influenced language design. Later languages increasingly treated features that constrained and clarified program structure as being just as important as syntactic expression.

BASIC and Interactive Programming

In 1963, John Kemeny and Thomas Kurtz began developing a language and time-sharing computing environment at Dartmouth College that could be easily learned by students who were not computer science specialists. As a result, the first BASIC program was executed on the Dartmouth Time-Sharing System on May 1, 1964.

BASIC used simple English-like commands, line numbers, and an interactive execution model. Instead of submitting punched cards and waiting a long time for results, users could enter programs at a terminal and immediately view the output. During the 1970s and 1980s, many personal computers included some form of BASIC interpreter by default, making BASIC one of the first programming languages encountered by the general public.

Simula and Object-Oriented Programming

Norwegian researchers Ole-Johan Dahl and Kristen Nygaard developed Simula to simulate complex real-world systems. The original Simula was a simulation language based on ALGOL, while Simula 67 later systematized concepts corresponding to classes, objects, inheritance, virtual methods, and dynamic dispatch.

Objects in Simula combined data and the operations that processed that data into a single unit. Objects were used to represent individual actors or components within a real-world system, while classes defined their shared structure. This model became a central foundation for later object-oriented languages, including Smalltalk, C++, Java, and C#. Simula is generally regarded as the first object-oriented programming language.

The 1970s: The Establishment of Major Programming Paradigms

During the 1970s, several programming paradigms that remain influential today became established in the form of independent languages. C emerged in systems programming, Pascal in education and structured programming, Smalltalk in object-oriented environments, Prolog in logic programming, and ML in statically typed functional programming.

Pascal

In 1970, Niklaus Wirth introduced Pascal, based on the structure of ALGOL 60. Pascal expressed the principles of structured programming through clear syntax and strong type checking, while providing arrays, records, sets, pointers, and user-defined data types.

Pascal was widely used in programming education, but it was not designed solely as an educational language. Wirth believed that a language should be based on simple fundamental principles while still being capable of expressing large programs efficiently. Pascal later influenced Modula-2, Oberon, Object Pascal, and a range of educational and commercial development environments.

C and UNIX

Dennis Ritchie developed C (programming language) at Bell Labs between 1971 and 1973, drawing influence from BCPL and B (programming language). C was designed as a systems programming language for implementing the early UNIX operating system.

Existing operating systems were generally written in assembly language, but when a substantial portion of UNIX was rewritten in C, it became easier to port the operating system to different computer architectures. C provided pointers, direct memory access, bitwise operations, and a simple runtime model, allowing programmers to retain low-level control over hardware while also using higher-level constructs such as functions, data types, and structures.

C kept the language itself relatively small and separated features such as input and output, strings, and memory management into libraries. This structure made it suitable for implementing operating systems, compilers, databases, embedded software, and the runtimes of other languages. The syntax and execution model of C later influenced many languages, directly or indirectly, including C++, Objective-C, Java, C#, JavaScript, Go (programming language), Rust, and Wave (programming language).

Smalltalk

In 1972, a research team at Xerox PARC that included Alan Kay, Dan Ingalls, and Adele Goldberg developed an early version of Smalltalk. Smalltalk was not merely a programming language, but an integrated environment combining an object-oriented language, a virtual machine, a source-code browser, a debugger, and a graphical user interface.

In Smalltalk, nearly every element—including numbers, strings, classes, and the execution environment itself—was treated as an object, while computation was expressed through message passing between objects. Developers could modify objects and code within a running system and immediately observe the results. Smalltalk’s object model, virtual machine, interactive development model, and graphical environment had a major influence not only on later object-oriented languages but also on modern integrated development environments and user interfaces.

Prolog

In 1972, Alain Colmerauer and his research group in Marseille, France, developed the first implementation of Prolog. Rather than writing a program as a sequence of commands to be executed, Prolog expressed programs as facts, rules, and queries to be solved.

The Prolog execution engine searches for solutions that satisfy given conditions using logical unification and backtracking. This approach formed a separate paradigm known as Logic programming and was used in research on natural-language processing, knowledge representation, expert systems, rule-based systems, and constraint solving.

ML and Statically Typed Functional Languages

Robin Milner designed ML as a metalanguage for writing proof procedures in the LCF theorem-proving system. ML was a higher-order functional language in which functions could be treated as values, while also providing a polymorphic static type system capable of inferring most types without requiring programmers to write them explicitly.

ML’s type inference and parametric polymorphism demonstrated that strong type safety could be maintained while reducing the verbosity of source code. Pattern matching, algebraic data types, and module systems in ML later influenced Haskell, OCaml, F#, Scala, and modern statically typed languages.

SQL and Declarative Languages

As relational databases developed, specialized declarative languages for processing data also emerged. SEQUEL, developed at IBM during the 1970s by Donald Chamberlin and Raymond Boyce, was later renamed SQL.

Rather than specifying in detail the sequence in which data should be searched and joined, SQL users describe the conditions and form of the desired result. The database management system determines the actual access path and execution plan. Although SQL serves a different purpose from general-purpose programming languages, it established the model of declaring desired results rather than explicit processing procedures in practical large-scale systems.

The 1980s: Personal Computers, Modularization, and the Spread of Object-Oriented Programming

During the 1980s, the spread of personal computers greatly expanded the population of programming-language users. BASIC was widely used on early personal computers, while Pascal and C compilers became common in education and commercial software development. Development environments that combined an editor, compiler, and debugger into a single product also made the process of writing and executing programs faster.

As programs grew in scale, organizing code as a simple collection of procedures was no longer sufficient. It became important to define public interfaces between modules, hide implementations, and safely combine separately compiled components. Wirth’s Modula-2 and Oberon, along with several Pascal-family languages, addressed modularization and interface checking at the language level.

C++

In 1979, Bjarne Stroustrup began developing “C with Classes” at Bell Labs. The language attempted to combine the class and object abstractions of Simula with the performance and systems-control capabilities of C. It was renamed C++ around 1983, and its first commercial implementation and language book were released in 1985.

C++ gradually added not only classes and inheritance, but also function and operator overloading, constructors and destructors, templates, exception handling, and generic programming. It developed into a multi-paradigm language that supported the combination of several programming styles while retaining substantial compatibility with C. It later became widely used in large applications, operating-system components, game engines, browsers, and high-performance software.

Perl and Scripting Languages

In 1987, Larry Wall released Perl to automate report generation, text processing, and system-administration tasks in UNIX environments. Perl combined features from several tools and languages, including C, the shell, sed, and awk.

Perl deeply integrated regular expressions and string-processing capabilities into the language, allowing files and text to be manipulated with short programs. It later played an important role in early CGI-based web programming and system automation, while also influencing the model of building a language ecosystem around a large repository of extension modules.

Haskell

During the 1980s, several lazy functional languages were being researched, but differences in syntax and features made it difficult to share research results and libraries. To address this issue, committee work during the late 1980s led to the design of Haskell.

Haskell adopted pure functional programming, in which side effects are separated through types and structure, together with lazy evaluation, algebraic data types, pattern matching, and type classes. In addition to commercial use, Haskell became an important foundation for experimenting with new type systems and abstraction mechanisms in programming-language research.

The 1990s: The Internet, Virtual Machines, and the Growth of Dynamic Languages

During the 1990s, improvements in personal-computer performance, graphical operating systems, and the spread of the Internet greatly changed the environments that programming languages needed to address. It became increasingly important to distribute programs across networks, execute the same code on different operating systems, and develop rapidly changing applications within short periods of time.

Python

In the late 1980s, Guido van Rossum envisioned a language at CWI in the Netherlands that would retain the strengths of the educational language ABC while being easily extensible for UNIX environments and practical systems programming. He began its implementation at the end of 1989 and released Python in the early 1990s.

Python used indentation to express block structure and provided dynamic typing, automatic memory management, exception handling, modules, and rich built-in data types. Its ability to connect easily with external modules written in C helped expand its use from simple scripts to scientific computing, web servers, automation, and artificial intelligence.

Java

In the early 1990s, James Gosling and researchers at Sun Microsystems investigated a software platform for network-connected consumer devices and embedded systems. They initially used C++, but created a new language and execution environment to address portability, reliability, memory management, and security concerns.

Released in 1995, Java adopted a model in which source code is compiled not into machine code for a particular CPU, but into bytecode executed by the Java Virtual Machine. By providing automatic memory management, runtime verification, and a standard library together, Java emphasized platform independence, allowing one program to run across multiple operating systems. Java began with web applets and later expanded into servers, enterprise software, mobile applications, and large-scale distributed systems.

JavaScript

In 1995, Brendan Eich at Netscape designed a language for responding to user input and dynamically changing the contents of web documents. The language was initially developed under other names, but was ultimately included in Netscape Navigator as JavaScript.

Despite its similar name, JavaScript is a separate language from Java. It uses dynamic typing, prototype-based objects, and first-class functions. Initially, it was primarily used for short scripts embedded in web pages. However, improvements in browser-engine performance, ECMAScript standardization, and the emergence of Node.js and module ecosystems allowed it to grow into a general-purpose language used for both frontend and server-side development.

PHP and Ruby

In 1994, Rasmus Lerdorf created CGI tools to manage visitor records on his personal website. These tools evolved through PHP/FI into PHP. PHP spread rapidly in dynamic website development through its simple deployment model and its ability to embed server-side code directly within HTML documents.

Yukihiro Matsumoto designed Ruby by combining influences from Perl, Smalltalk, LISP, and other languages, with an emphasis on developer expressiveness and convenience. Ruby was developed beginning in 1993 and released in 1995. It provided a consistent object model in which all values were treated as objects, together with blocks and metaprogramming features. It later became widely known in web application development following the emergence of Ruby on Rails.

The 2000s: Managed Platforms and Multi-Paradigm Languages

During the 2000s, virtual machines and managed runtimes became central technologies in large-scale software development. A programming language increasingly came to be understood not merely as source-code syntax, but as an entire ecosystem including a garbage collector, standard libraries, package management, debuggers, JIT compilers, and deployment platforms.

C# and .NET

Microsoft developed C# for the .NET platform under the leadership of Anders Hejlsberg. The first widely distributed implementation was introduced alongside the .NET initiative in 2000.

C# used C-family syntax and an object-oriented model while integrating automatic memory management, runtime type information, properties, delegates, and events. It later added generics, LINQ, lambda expressions, asynchronous functions, pattern matching, and records, developing into a language that supports object-oriented, functional, and declarative programming together.

Scala

Martin Odersky began developing Scala at EPFL in 2001 and released an implementation for the JVM in 2004. Scala combined object-oriented and functional programming within a single static type system while maintaining interoperability with the Java Virtual Machine and Java libraries.

Scala provided higher-order functions, pattern matching, immutable data structures, traits, and powerful type inference. Its strategy of introducing new language features while using the ecosystem of an existing platform was later repeated by several modern languages targeting the JVM and .NET.

The 2010s: Concurrency, Memory Safety, and Large-Scale Development

Beginning in the late 2000s, processor development shifted from increasing the speed of a single core toward multicore and parallel processing. At the same time, Internet services grew in scale and cloud infrastructure spread, making concurrency, reliable deployment, fast builds, and the management of large codebases central concerns in programming-language design.

Go

In 2007, Robert Griesemer, Rob Pike, and Ken Thompson at Google began designing Go (programming language) to address long compilation times, complex dependency relationships, and concurrency problems encountered in large-scale software infrastructure development. The Go project was publicly announced in 2009, and Go 1 was released in 2012.

Go provided a relatively small syntax, fast compilation, garbage collection, a package system, goroutines, and channels. Rather than introducing a large number of language features, it integrated standard tools, formatting, testing, modules, and build procedures in order to simplify team-based development of server and network software.

Rust

Rust, initiated by Graydon Hoare and later developed by Mozilla and the open-source community, aimed to provide memory safety and concurrency safety in a systems programming language without relying on a garbage collector. Rust 1.0 was released in 2015 together with a stable language, standard library, and the Cargo package and build tool.

Rust uses ownership, borrowing, and lifetime analysis to check memory-usage rules at compile time. It seeks to prevent several classes of errors, including data races and access to freed memory, during compilation while retaining detailed control and performance comparable to C and C++. Rust also adopted an edition system that allows its syntax to evolve gradually while preserving compatibility with existing code.

Kotlin

JetBrains began the Kotlin project in 2010 and publicly announced it in 2011. Kotlin was designed to remain compatible with existing Java code and the JVM ecosystem while providing modern features such as null safety, type inference, extension functions, data classes, and coroutines.

Kotlin 1.0 was released in 2016. The language later expanded beyond the JVM into a multiplatform language targeting JavaScript, native code, and WebAssembly.

Swift

Apple developed Swift to modernize its Objective-C-based development environment and released it in 2014. Swift provided static typing, type inference, value types, optionals, generics, and protocol-oriented abstractions.

It was initially used primarily for iOS and macOS application development, but was later made open source and expanded to additional platforms, including Linux and Windows, as well as server and embedded environments.

TypeScript

As web applications grew larger, demand increased for checking the structure and errors of dynamically typed JavaScript code during development. Released by Microsoft in 2012, TypeScript was designed as a superset of JavaScript that allowed optional static type annotations to be added while preserving existing code.

TypeScript uses a structural type system and control-flow analysis and produces ordinary JavaScript as its compilation output. It became a leading example of a successful gradual type system that added a static-analysis layer on top of an existing large runtime environment and ecosystem rather than replacing them.

WebAssembly

In 2019, the World Wide Web Consortium adopted the core WebAssembly specification as a web standard. WebAssembly is not generally a high-level language written directly by humans, but rather a safe and portable low-level bytecode and virtual-machine instruction format generated from languages such as C, C++, and Rust.

This gave web browsers a common target capable of efficiently executing compiled output from multiple languages in addition to JavaScript. WebAssembly later expanded beyond browsers into servers, plugins, sandboxes, and portable execution environments, further broadening the relationship between programming languages and execution platforms.

The 2020s and Beyond

Programming languages of the 2020s are becoming increasingly difficult to classify according to a single paradigm or execution model. Modern languages commonly provide procedural, object-oriented, functional, declarative, and concurrent models together as needed. Statically typed languages are strengthening type inference and interactive tooling, while dynamically typed languages are introducing optional type checking and static-analysis capabilities.

Major directions in language design include the following:

  • Memory safety and the reduction of undefined behavior
  • Asynchronous execution and structured concurrency
  • Explicit handling of null values and errors
  • Type inference and gradual type systems
  • Greater use of immutable data and functional expressions
  • Standardization of package managers, build systems, and formatters
  • Multiplatform execution across multiple operating systems and CPU architectures
  • Interoperability with existing C ABI, JVM, .NET, JavaScript, and WebAssembly ecosystems
  • Heterogeneous computing across CPUs, GPUs, and accelerators
  • Open development of language specifications and implementations, together with community governance

Compiler architecture has also increasingly moved away from a simple model in which one source language is translated directly into one machine language. Instead, compilers commonly use multiple levels of intermediate representation. Compiler infrastructure such as LLVM and MLIR allows language frontends, optimizers, and CPU or GPU backends to be reused, with the goal of reducing the development cost of Domain-specific languages for particular hardware platforms or computational domains.

The emergence of new languages does not cause existing languages to disappear immediately. Fortran and COBOL remain in use in scientific and business systems built up over several decades, while C and C++ maintain vast ecosystems in operating systems and high-performance software. Java and C# continue to gain new capabilities around managed platforms, while Python and JavaScript continue to develop improvements in runtime performance and type analysis while preserving their existing code and package ecosystems.

The history of programming languages can therefore be understood as a sequence of responses to the performance and architecture of contemporary computers, the range of their users, the scale of the software being developed, and the required levels of safety and productivity. Modern programming languages continue to evolve by combining these historical lineages, while languages at many different levels—from low-level languages close to machine control to declarative and domain-specific languages close to human problem expression—are used in complementary roles.

Classification and Components

Programming languages cannot be clearly divided according to a single criterion. A language may simultaneously be a high-level language, a statically typed language, support object-oriented, procedural, and functional programming, and target both native code and virtual machines. Therefore, the classification of programming languages should be understood not as a set of mutually exclusive categories, but as a multidimensional system that explains languages from different perspectives, including their level of abstraction, execution model, type system, programming paradigms, intended use, and resource-management model. The ACM Computing Classification System likewise uses a structure in which functional, object-oriented, logic and constraint, data-flow, parallel and distributed, and multi-paradigm languages are treated as separate categories, while allowing multiple classifications to be applied to a single subject of study.

The main classification criteria can be summarized as follows.

CriterionClassification examplesWhat is distinguished
Level of abstractionMachine code, assembly language, high-level languageHow directly the language expresses hardware details
Execution modelAhead-of-time compilation, interpretation, bytecode, JITHow source code is transformed into an executable form
Type systemStatic and dynamic typing, explicit and inferred typingWhen and how the validity of values and operations is checked
Type relationshipsNominal typing, structural typingHow compatibility between different types is determined
ParadigmImperative, object-oriented, functional, logic, data-flowHow computation and program structure are expressed
Intended useGeneral-purpose language, domain-specific languageThe range of problem domains the language is designed to address
Resource managementManual management, garbage collection, ownership-based managementHow the lifetimes of memory and external resources are managed
Concurrency modelThreads, shared memory, message passing, channels, coroutinesHow the execution and synchronization of multiple tasks are expressed

Classification by Level of Abstraction

Machine code is the binary representation of instructions interpreted directly by a processor. Each instruction represents an operation code and operands, registers, or memory addresses belonging to a particular instruction set architecture. Machine code allows the most direct control of hardware, but it is difficult for humans to read and write, and even the same program must be reconstructed when the target processor changes.

Assembly language represents machine instructions using symbols such as MOV, ADD, and JMP instead of numbers. Labels, constants, and macros can be used, making it easier to read and manage than machine code. However, it is classified as a low-level language because it directly deals with the structure of the target architecture, including registers, calling conventions, memory-addressing methods, and the instruction set.

High-level programming language expresses programs primarily through concepts closer to problem solving, such as algorithms, data structures, functions, objects, and modules, rather than hardware instructions. The degree of abstraction differs between languages, ranging from languages such as C, which allow direct manipulation of memory addresses and pointers, to languages such as Python and Haskell, which delegate a substantial portion of execution and data representation to the language implementation. Therefore, the distinction between low-level and high-level languages is closer to a continuous spectrum based on hardware dependence and degree of abstraction than to an absolute two-level division.

In this context, the term high-level does not refer to the quality or difficulty of a language. It indicates how much the concrete behavior of hardware is abstracted, and a high-level language is not necessarily easy to use or slow to execute. A compiler can transform the structures of a high-level language into efficient machine code through intermediate representations and optimization stages. LLVM provides an architecture in which intermediate representation generated by a language frontend is analyzed and optimized before being translated into machine code for multiple target architectures.

Classification by Execution Model

Programming languages are often divided into compiled languages and interpreted languages, but this distinction is closer to describing how a particular implementation executes a program than an absolute property of the language itself. The same language may be compiled ahead of time into native code, translated into virtual-machine bytecode, compiled into machine code during execution, or executed by directly interpreting its syntactic structure, depending on the implementation. Python alone has multiple implementations, including CPython, PyPy, Jython, and IronPython. PyPy uses JIT compilation, while Jython and IronPython use the JVM and .NET execution environments, respectively.

Ahead-of-time compilation, or AOT compilation, transforms source code into machine code for a target system or another executable format before the program is run. In typical implementations of C and C++, an executable file or library is produced through preprocessing, syntactic and semantic analysis, intermediate-representation generation, optimization, target-specific code generation, assembly, and linking. In actual tools, however, several stages may be combined into a single command, or the generation of some intermediate files may be omitted.

An Interpreter reads a program representation at runtime and performs operations according to its meaning. An interpreter does not necessarily execute the original source code directly one line at a time. It may first convert the source code into tokens, a syntax tree, internal instructions, or bytecode, and then interpret that intermediate form.

In a model that uses Bytecode and a Virtual machine, source code is translated into a virtual instruction set rather than the instruction set of a physical CPU. Java source code is generally compiled into JVM class files and bytecode, while the JVM defines an execution structure and instruction set that include the stack, heap, method area, and constant pool. This model places a common execution layer between the language and physical hardware, allowing the same program format to be used across different operating systems and processors.

JIT compilation converts an intermediate representation or frequently executed code into machine code for the current system while the program is running. It can perform optimizations based on information collected during execution, such as actual call relationships, value shapes, and execution frequency, but the time and memory required for compilation become part of the runtime process. Modern language implementations may combine interpreters, JIT compilers, and AOT compilers, so compilation and interpretation are not completely separate methods, but implementation strategies that can be combined within a single execution system.

Classification by Type System

A Type system is a set of rules that defines the kinds of values that can be used in a program, the operations permitted on each value, and the relationships between values. For example, different operations may be permitted for integers and strings, and particular type conditions may be applied to function parameters and return values. The Go specification describes a type as a set of particular values together with the set of operations and methods that apply to those values, while in Haskell an expression represents a value and simultaneously has a static type.

A Static typing language checks type relationships among expressions, variables, function calls, and other elements before the program is executed. C, C++, Rust, Java, Haskell, and Go primarily use static type checking. Static type checking can detect some possible errors before execution and allows the compiler to use information about value representation and call structure for optimization. However, even in statically typed languages, conditions determined only at runtime, such as array bounds, external input, and dynamic loading, may need to be checked separately.

In a Dynamic typing language, values carry type information at runtime, and validity is often checked when an operation is actually applied to a value. Python, Ruby, and JavaScript are representative examples. Dynamic typing makes it possible to associate values of different kinds with the same name in sequence or to handle structures generated at runtime naturally, but some type errors may not appear until the relevant code path is actually executed. In Python's data model, all program data is represented as objects or relationships between objects, and each object has an identity, a type, and a value.

The distinction between static and dynamic typing is separate from whether type annotations are written explicitly. A statically typed language that supports Type inference can determine types by analyzing expressions and usage relationships even when the programmer does not specify every type. Haskell, ML, Rust, Kotlin, and Swift make extensive use of type inference. Conversely, a dynamic language may add a static-analysis layer that does not directly change runtime behavior, such as TypeScript type annotations or Python type hints.

Type compatibility can also be classified into Nominal typing and Structural typing. A nominal type system determines compatibility primarily from the names of type declarations or explicit inheritance and implementation relationships. A structural type system compares the actual structure of types, such as the fields and methods they contain, rather than their names. TypeScript uses structural subtyping, determining type compatibility according to the members of an object.

Type systems can additionally be distinguished by features such as generics and parametric polymorphism, subtyping, sum types and product types, algebraic data types, dependent types, and gradual typing. The commonly used expressions strong typing and weak typing are used with different meanings depending on the literature and the speaker, including implicit conversion, memory safety, runtime checking, and the ability to bypass the type system. Therefore, when precise classification is required, it is more appropriate to describe specific properties directly, such as static or dynamic checking, explicit or implicit conversion, and memory safety. Types and polymorphism do not form a single strong-to-weak scale, but consist of multiple independent rules that define value classification, abstraction, and compatibility.

Classification by Programming Paradigm

A Programming paradigm describes the concepts and structures through which computation is expressed. Most modern languages do not strictly follow only one paradigm, but are multi-paradigm languages that support several approaches together. For example, C++ supports procedural, object-oriented, generic, and functional forms of programming, while Python and JavaScript can also use objects, functions, and imperative control structures together. The ACM language classification likewise treats functional, object-oriented, logic and constraint, data-flow, concurrent and distributed, and multi-paradigm languages as separate categories.

Imperative programming expresses computation as a sequence of commands that modify program state. Values are assigned to variables, while execution flow is controlled through conditionals, loops, and function calls. Procedural programming organizes an imperative program into callable units such as functions, procedures, and subroutines. C, Pascal, and FORTRAN are commonly classified as representative procedural languages.

Object-oriented programming organizes data and the operations that process it into units called objects. In class-based languages, classes define the structure and behavior of objects, while encapsulation, inheritance, polymorphism, and dynamic dispatch are used to express relationships between objects. Java, C#, C++, and Smalltalk are representative examples, although each language defines objects, classes, and inheritance differently. Some languages, such as JavaScript, provide class declarations while internally using a prototype-based object model. The ECMAScript specification defines the computation and object-manipulation mechanisms of the language, while separating input and output features such as files and networks into the responsibilities of the host environment.

Functional programming treats functions as both the central units of computation and as values, organizing programs through function composition, recursion, higher-order functions, and immutable data structures. Pure functional languages emphasize functions that return the same result for the same input and the control of side effects. Haskell is organized around expressions, functions, and static types, while LISP, ML, F#, and many modern languages also support major concepts of functional programming. Functional programming has been studied as a computational model that replaces or complements traditional von Neumann-style programming centered on commands and storage state.

Logic programming describes facts, rules, and queries, while the execution engine searches for solutions that satisfy the conditions through logical inference. Prolog is the representative example and uses unification, backtracking, and relation-oriented descriptions. Constraint programming describes conditions that variables must satisfy, while a constraint solver searches for possible values. Both approaches are characterized by declaring the relationships and conditions that must hold rather than directly writing a concrete sequence of commands.

Declarative programming is a broad category in which the desired result or relationships are described rather than specifying the computational process through detailed commands. Functional and logic languages are sometimes included in the declarative category, and languages such as SQL, which describe the conditions and form of required data while leaving the actual access order and execution plan to the system, are also classified as declarative languages.

Data-flow programming represents data dependencies between operations as a graph and performs an operation when the required inputs become available. It is frequently used in visual programming environments, signal processing, parallel computation, and machine-learning graph systems. Reactive programming organizes programs around values that change over time, event streams, and propagation relationships between values.

Concurrent programming deals with structures in which multiple computations overlap in execution, together with the communication and synchronization between them. Depending on the language, it may provide operating-system threads and shared memory, actors and message passing, coroutines and asynchronous functions, or channel-based communication. Go defines channel communication as a primary synchronization method between goroutines and specifies ordering relationships between a channel send and its corresponding receive in the memory model.

Classification by Intended Use

A General-purpose programming language is designed to support the development of many kinds of software rather than being limited to a single problem. C, C++, Java, Python, Rust, and Go fall into this category. However, even a general-purpose language may be used more widely in certain areas, such as systems software, enterprise applications, web servers, or automation, depending on its design goals and ecosystem.

A Domain-specific language is designed to express a particular problem domain concisely and precisely. SQL expresses relational data queries and manipulation, regular expressions express string patterns, shader languages express GPU graphics operations, and hardware-description languages express digital circuits. A domain-specific language may run as an independent program or may be embedded within strings, libraries, macros, or built-in syntax of another language.

Languages may also be classified by field of use as systems programming languages, scripting languages, shell languages, data-query languages, scientific-computing languages, educational languages, hardware-description languages, shader languages, and concurrent or parallel programming languages. These names indicate the primary design purpose and common usage of a language rather than the full set of features it can support, so one language may belong to multiple fields.

Core Components of a Language

A programming language is not composed of syntax alone. To function as a complete computational system, it must define the characters and tokens that make up source code, the structure of statements, the behavior represented by each statement, values and types, name scope, control flow, abstraction mechanisms, and methods of memory and error handling. The ACM classification system lists data types and data structures, control structures, functions and procedures, classes and objects, modules and packages, polymorphism, recursion, concurrency structures, and dynamic storage management as major components of programming languages.

Lexical Structure and Tokens

Lexical structure defines the smallest grammatical units that make up source code. These include identifiers, keywords, literals, operators, delimiters, comments, and rules for handling spaces and line breaks. A lexical analyzer reads a sequence of input characters and converts it into a sequence of tokens that can be processed by a parser.

The meaning of whitespace differs between languages. In C-family languages, most whitespace serves only to separate tokens, while in Python changes in indentation are converted into INDENT and DEDENT tokens that represent block structure. The Python reference documentation distinguishes between lexical analysis, which processes character input, and syntactic analysis, which processes the resulting tokens.

Syntax

Syntax defines the order and structure in which tokens must be combined to form a valid program. Syntax rules include the forms of variable declarations, function definitions, arithmetic expressions, conditional statements, loops, type declarations, and module declarations. Language specifications generally describe syntax using grammatical notations such as BNF, EBNF, PEG, or an equivalent formal system.

Syntax rules determine only the external structure of a program. For example, even if a + b is a syntactically valid expression, whether addition can be applied to a and b, and what the result of that operation is, are determined by type rules and semantics.

Semantics

Semantics defines what a syntactically valid program means and how it behaves. Language standards normally specify semantics together with syntax, and the COBOL standard likewise defines the syntax and semantics of the language in order to support machine-independent programs.

Static semantics deals with rules that can be checked before the program is executed. These include whether names have been declared, whether the scope of a variable is valid, whether function arguments and return types match, and whether a control statement appears in an allowed position. Type checking and name-resolution rules that are difficult to express through syntax alone belong to static semantics.

Dynamic semantics defines what state changes each expression and command produces and what values are calculated when the program is executed. The evaluation order of operators, function calls, exception raising, object creation, memory access, and the ordering of concurrent execution are included in dynamic semantics. Semantics may be explained in natural language or defined using mathematical methods such as operational semantics, denotational semantics, and axiomatic semantics.

Values and the Data Model

A Data model defines what values are in a language and what kinds of values can be created and combined. It may include not only primitive values such as integers, real numbers, Boolean values, characters, and strings, but also arrays, tuples, records, structures, lists, sets, maps, functions, classes, and objects.

The relationship between values and objects differs between languages. Python models all data as objects or relationships between objects and defines each object as having an identity, a type, and a value. Haskell treats expressions as representations of values and assigns each expression a static type. C defines objects, values, and storage regions through a memory model closer to hardware. These differences in data models affect assignment and copying, identity comparison, mutability, and memory layout.

Names, Binding, and Scope

Identifiers in a program refer to entities such as variables, functions, types, and modules. The connection between an identifier and the actual entity is called Binding. A binding may be formed at compile time, link time, program-load time, or during execution.

Scope is the region of source code in which a particular name can be referenced, while Lifetime is the period during execution in which a value, object, or storage region actually exists. The two concepts are related but not identical. An object that has gone out of scope may continue to exist through another reference, while a resource once referred to by a name may no longer be valid even though that name remains within scope.

A language may provide global scope, module scope, function scope, block scope, class scope, and other forms. In nested functions, a Closure may be created that captures local variables from an enclosing function, while the order in which names are resolved and the method of capture vary according to the execution model of the language.

Expressions and Statements

An Expression is a language element that calculates or represents a value. Literals, variable references, operator applications, function calls, conditional expressions, and object creation may all be expressions. A Statement, or command, represents an operation that affects program execution flow or state, such as assignment, iteration, branching, return, or exception handling.

Some languages treat conditional constructs and blocks as expressions that produce values, while others strictly distinguish between expressions that produce values and statements that only perform actions. The Python language reference defines the syntax and semantics of expressions separately, while Haskell treats expressions as the central computational units of the language.

Control Flow

Control flow determines which parts of a program are executed and in what order. Sequential execution, conditional branching, iteration, function calls and returns, recursion, and exception handling are fundamental control structures.

Depending on the language, advanced control structures such as pattern matching, coroutines, generators, asynchronous functions, continuations, and nondeterministic choice may also be provided. These structures are not merely syntactic conveniences, but determine program state, the call stack, value-evaluation order, and the way errors propagate.

Functions and Abstraction

Functions, procedures, and subroutines separate repeated computations into named units. Parameters, return values, local variables, calling conventions, and value-passing methods are major elements of a function system. A language supports First-class function when functions can be used as arguments or return values of other functions, while a function that accepts or returns another function is called a Higher-order function.

In addition to functions, languages provide abstraction of data and behavior through classes, interfaces, traits, protocols, type classes, and abstract data types. Polymorphism allows a single interface or piece of code to be applied to values of multiple types and can be divided into overloading, subtype polymorphism, and parametric polymorphism.

Modules and Packages

A Module groups declarations of related values, functions, types, classes, and other elements into a single namespace and distribution unit. A module distinguishes between items exposed externally and internal implementation details and provides a structure for importing the features of other modules. Haskell modules define values, data types, type synonyms, classes, and other items, export selected elements, and use import declarations to include elements of other modules within scope.

A package is often used as a larger distribution unit that groups one or more modules, resources, and metadata. However, the exact meaning and relationship of modules and packages differ between languages and toolchains.

Memory and Resource Management

A program must allocate memory for storing values and objects and reclaim memory that is no longer needed. In C and C++, programmers directly allocate and release memory or use object-lifetime rules. Java, C#, Go, Python, and other languages primarily use Garbage collection, in which the execution environment identifies and reclaims unreachable objects.

Rust uses ownership, borrowing, and lifetime rules checked by the compiler to provide memory safety without a garbage collector. Resources are cleaned up when the owner of a value goes out of scope, while situations in which a reference outlives the original value or conflicting mutable references are created are restricted at compile time.

In addition to memory, resources such as files, network connections, locks, and GPU resources may require explicit release. Languages manage the lifetimes of these resources using mechanisms such as destructors, cleanup statements, context managers, deferred execution, and patterns that combine resource acquisition with initialization.

Errors and Exception Handling

Languages provide ways to express and propagate failures. They may use traditional return codes and global error states, or they may raise exceptions that interrupt the current call flow and transfer control to a higher-level handler. Some statically typed languages represent successful values and error values through sum types or result types, requiring callers to handle the possibility of failure through the type system.

Error-handling methods affect function return types, control flow, resource cleanup, and program stability. Even languages that provide exceptions may treat recoverable errors, programming defects, and system-termination conditions in different ways.

Concurrency and Asynchronous Execution

A concurrency model defines how multiple tasks are created, how data is exchanged between them, and how execution order is coordinated. In languages that use threads and shared memory, locks, atomic operations, and memory-ordering rules are important. In message-passing or channel-based models, the communication process through which tasks exchange values becomes the center of synchronization.

Coroutines and async·await allow a task to yield execution while waiting for input and output or another asynchronous operation and later resume from the point where it was suspended. These features do not necessarily imply actual parallel execution and may alternate multiple tasks on a single thread or be combined with multiple threads.

Standard Library and Execution Environment

A standard library provides commonly needed functions such as strings, collections, files, mathematics, time, networking, and concurrency through shared interfaces. The core language specification and the standard library are closely related but are not identical. The Python language reference also distinguishes the rules of the language itself from the functions of built-in and standard modules.

A Runtime is the software layer that supports language features while a program is running. It may handle memory allocation and garbage collection, dynamic type checking, exception handling, thread management, virtual-method calls, and reflection. There may be an independent virtual machine, such as the JVM, that specifies execution memory and a virtual instruction set, or a structure such as C that uses a comparatively small compiler runtime and system libraries.

A language may leave input, output, and operating-system functions to the execution environment instead of defining them directly. The ECMAScript specification defines the language's computational and object models, but external data input and result output are provided by a host environment such as a browser or server runtime. Therefore, an actual development environment is formed through the combination of the language specification, runtime, standard library, and host APIs.

Language Processing Systems and Tools

A language processing system may include a lexical analyzer, parser, semantic analyzer, compiler, interpreter, optimizer, code generator, assembler, linker, and loader. An actual compilation toolchain analyzes source code to construct an abstract syntax tree, lowers it into an intermediate representation, performs optimization and target-specific code generation, and links the resulting object files and libraries into an executable program.

A development environment includes debuggers, static analyzers, formatters, linters, testing tools, documentation generators, build systems, and package managers. These tools are not syntactic parts of the language, but they have a major effect on practical usability, ecosystem stability, code portability, and reproducibility.

Distinguishing Languages, Specifications, and Implementations

A programming language should be understood by distinguishing between its language specification, implementation, standard library, and development ecosystem. A Language specification defines which programs are valid and what meaning each program has. An Implementation is software such as a compiler, interpreter, or virtual machine that makes it possible to execute programs conforming to that specification.

A single language may have multiple implementations, and each implementation may differ in internal structure, performance, execution targets, and additional features. In addition to CPython, Python has multiple implementations such as Jython for the JVM, IronPython for .NET, and PyPy, which uses a JIT compiler. Conversely, one execution environment may support multiple languages. The JVM can execute Kotlin, Scala, and Clojure in addition to Java, while .NET can execute C#, F#, and Visual Basic on a common intermediate language and runtime.

Therefore, when describing the characteristics of a particular programming language, it is more appropriate to examine its syntax and semantics, type system, paradigms, execution model, memory management, concurrency structure, and major implementations together rather than assigning it only a single label such as “compiled language,” “object-oriented language,” or “statically typed language.” The combination of these elements determines the forms of programs that can be expressed in the language, the development methods it supports, its safety, performance, and fields of use.

Programming Paradigms

A Programming paradigm refers to a fundamental perspective and method for viewing and expressing programs and computation. Some paradigms describe programs as sequences of commands that modify memory state, while others describe them in terms of function evaluation, message exchange between objects, logical relationships, data dependencies, or streams of events. A paradigm is a design model that determines how a program is divided into units, where state and control flow are placed, and how problems are abstracted.

Programming languages and programming paradigms do not correspond one-to-one. A single language may support multiple paradigms, and the same paradigm may be implemented through different syntax and execution models in different languages. C is primarily used as a procedural and imperative language, but other forms of organization are also possible through function pointers and abstract data types. C++ supports procedural, object-oriented, generic programming, and functional expressions together, while Python and JavaScript likewise allow imperative code, objects, first-class functions, and higher-order functions to be used together. Therefore, rather than defining a particular language solely through one paradigm, it is more accurate to examine which paradigms the language was primarily designed around and to what extent it supports each one.

The major programming paradigms can be summarized as follows.

ParadigmCentral view of computationRepresentative components
Imperative programmingA sequence of commands that modify stateVariables, assignment, conditionals, loops
Procedural programmingDecomposition of commands into callable proceduresFunctions, procedures, subroutines
Object-oriented programmingCooperation between objects with state and behaviorObjects, messages, classes, interfaces
Functional programmingEvaluation and composition of functionsPure functions, higher-order functions, immutable values
Logic programmingInferring results from facts and rulesRelations, rules, queries, unification
Constraint programmingDescribing conditions that values must satisfyVariables, domains, constraints, solution search
Data-flow programmingExecuting operations according to data dependenciesNodes, connections, tokens, data graphs
Reactive programmingPropagation of changes occurring over timeEvent streams, signals, subscriptions, propagation
Concurrent programmingProgress and interaction of multiple computationsThreads, messages, channels, actors
Generic programmingAlgorithms independent of types and implementationsType parameters, constraints, concepts
Aspect-oriented programmingSeparation of concerns that span multiple modulesAspects, join points, advice, weaving

These classifications are not mutually exclusive. A functional program may simultaneously be declarative and concurrent, while procedural algorithms and generic data structures may be used within an object-oriented language. Models such as data flow, reactive programming, and the actor model may be implemented as paradigms for an entire language, but they may also be used as partial programming models provided by a particular library or framework.

Imperative Programming

Imperative programming expresses a program as a sequence of commands that modify the state of a computer. A program reads the current state, records computational results in variables or memory, and selects the next command to execute according to conditions. A typical imperative program consists of variables, assignment statements, conditional statements, loops, function calls, and input/output commands.

The basic model of imperative programming is closely connected to the structure of traditional stored-program computers, which read values from memory, perform operations, and write the results back to memory. In his 1977 Turing Award lecture, John Backus analyzed how existing imperative languages strongly reflected the von Neumann model of computation, in which values in storage are changed one at a time, and proposed an alternative programming model centered on function composition. This discussion became a representative theoretical basis for explaining the distinction between imperative and functional programming.

The following code shows a typical imperative structure for calculating a sum.

int total = 0;

for (int i = 0; i < count; ++i) {
    total += values[i];
}

In this code, the values of total and i continue to change during execution. To understand the result of the program, it is necessary to trace how the state of each variable changes after every command is executed.

Imperative programming corresponds naturally to hardware behavior and allows fine-grained control over execution order, memory use, and side effects. It is widely used in fields where execution behavior and resource management are important, such as operating systems, device drivers, game engines, embedded systems, and high-performance software. However, when shared state is modified from multiple locations, the behavior of the code may become difficult to reason about, and race conditions and synchronization problems may occur in concurrent execution environments.

Procedural Programming

Procedural programming is a method of dividing an imperative program into callable units known as functions, procedures, or subroutines. Each procedure performs a particular task, receives values through parameters, and either returns a result or changes program state.

Procedural programming decomposes a large problem into several smaller procedures. For example, a program that processes files may be divided into procedures for opening a file, reading a header, interpreting data, saving results, and handling errors. Each procedure can expose only the interface required by its caller while hiding its internal implementation details.

C, Pascal, FORTRAN, and BASIC are commonly classified as representative procedural languages. Even in modern object-oriented and multi-paradigm languages, algorithms inside methods or ordinary functions are often written in a procedural style.

The core of procedural programming is not simply the use of functions. It lies in organizing the execution flow of a program through clear relationships between procedure calls and separating the responsibilities, inputs, and outputs of each procedure. Appropriate procedural decomposition improves code reusability and testability, but excessive dependence on mutable state shared across the entire program can increase coupling between procedures.

Structured Programming

Structured programming is an approach that seeks to organize program control flow through clear structures such as sequential execution, selection, and iteration while reducing unstructured branches that jump to arbitrary locations. Its purpose is to make program behavior easier to understand and verify locally by expressing control flow through nestable blocks and procedures.

Researchers including Edsger Dijkstra argued that as programs grow larger, structures that allow humans to reason accurately about execution paths become increasingly important. Dijkstra's *Notes on Structured Programming* is a major work that systematized methods for decomposing programs step by step and organizing them through a limited set of control structures.

Structured programming is often explained merely as avoiding the use of goto statements, but its core is not the prohibition of a particular syntax. Its essence lies in dividing a program into small units that can be understood at once and in making the entry point, exit point, preconditions, and results of each unit clear. Most block structures, functions, conditional statements, loops, and exception-handling mechanisms in modern languages were influenced by structured programming.

Object-Oriented Programming

Object-oriented programming is a paradigm that organizes a program as a collection of objects with state and behavior and through interactions between those objects. An object combines the data it manages and the operations applied to that data into a single unit. Other objects interact with it through a public interface or messages, while its concrete internal representation can be structured so that outside code does not need to know it directly.

The origins of object-oriented programming lie in Simula, developed during the 1960s. Simula introduced concepts corresponding to objects, classes, and inheritance in order to represent real-world actors and processes in discrete-event simulations. Simula 67 systematized concepts that later became central to object-oriented languages, including classes, objects, and virtual procedures.

Smalltalk in the 1970s expanded objects and message passing into the central organizing principles of the entire program. In Smalltalk, nearly every element, including numbers, collections, classes, and the development environment itself, is treated as an object, and computation is expressed through objects sending messages to one another. Smalltalk had a major influence not only on object-oriented languages but also on the development of graphical user interfaces and interactive development environments.

The following concepts are generally considered important in object-oriented programming.

  • An object is a unit within a program that has identity, state, and behavior.
  • Encapsulation separates an object's internal state and implementation from its external interface.
  • Message passing, or method invocation, is the means by which one object requests an operation from another.
  • Polymorphism allows different implementations to be used through the same interface.
  • Inheritance is a method of defining a new type based on the characteristics of an existing type or class.
  • Composition is a method of combining multiple objects to construct larger behavior.

Not every object-oriented language provides these elements in the same way. Java and C# center on explicit classes and interfaces, while C++ uses classes together with value types and templates. JavaScript's object model has traditionally been based on prototype links, and the language's class syntax is also built internally on prototype-based behavior. Go does not provide traditional class inheritance, but supports object-oriented design through methods, interfaces, and composition.

Inheritance is only one tool in object-oriented design and is not an essential central element. More fundamental goals are to divide object responsibilities clearly, provide stable external interfaces, and manage dependencies between objects. Excessively deep inheritance hierarchies and broad sharing of mutable state can increase coupling, so modern object-oriented design often uses interfaces, composition, immutable objects, and dependency injection together.

Functional Programming

Functional programming is a paradigm that expresses computation through the evaluation and composition of functions. A function is treated not merely as a procedure that groups commands, but as a first-class value that can be stored in a variable, passed as an argument, and returned as a result like any other value.

The theoretical foundations of functional programming include the Lambda calculus established by Alonzo Church. Early LISP introduced major concepts of functional programming into a practical programming language by centering on functions, recursion, and list processing. Later, ML-family languages developed static type inference and algebraic data types, while Haskell established a representative model of pure functional languages centered on pure functions, lazy evaluation, and type classes. Haskell originated in discussions during the 1980s about the need for a common public language for different lazy functional languages.

Important concepts in functional programming include the following.

  • A first-class function allows a function to be stored and passed like other values.
  • A higher-order function accepts a function as an argument or returns a function as its result.
  • A pure function does not modify external state and returns the same result for the same input.
  • Referential transparency is the property that replacing an expression with its resulting value does not change the meaning of the program.
  • Immutability uses the creation of new values rather than modifying values that have already been created.
  • Recursion expresses repeated computation through a function calling itself.
  • Function composition combines small functions to construct more complex computation.
  • Pattern matching branches computation according to the structure of a value and extracts its internal elements.

The following is a typical functional expression that processes a collection through function composition.

sumOfSquares =
    sum
    . map (\x -> x * x)
    . filter even

This expression does not directly specify the order in which the collection should be traversed or what values should be assigned to temporary variables. It represents the relationship between transformations: selecting even numbers, squaring each value, and then summing the results.

Organizing a program around pure functions and immutable values can reduce the effects that functions have on external state, making it easier to reason about code locally and test functions independently. Because shared mutable state is reduced, it is also advantageous for limiting data races in concurrent programs.

However, not every functional language permits only pure functions. LISP, OCaml, F#, and Scala provide functional features together with mutable state and imperative control structures. Haskell can also express input/output and state changes, but distinguishes such effects from pure expressions through types and computational structures. Functional programming can therefore be understood not as the complete elimination of side effects, but as a way of clearly controlling where side effects occur and how they are related.

Functional languages also differ according to evaluation strategy. Strict evaluation computes function arguments before invocation, while non-strict or lazy evaluation can defer computation until the result is actually needed. Lazy evaluation can naturally express infinite data structures and the composition of computations, but it can also make the actual evaluation point and memory use difficult to predict.

Declarative Programming

Declarative programming is a broad approach that describes desired results, relationships, or constraints rather than directly listing the specific sequence of commands required to perform a computation. Whereas an imperative program centers on “in what order should this be computed,” a declarative program centers on “what result should hold.”

Declarative programming does not refer to one single execution model. Functional programming, logic programming, constraint programming, rule-based programming, and data-query languages can all be included in the broader declarative paradigm. However, imperative features may also be used within functional languages, and the execution engine of a declarative language may internally use complex imperative algorithms, so the boundary between declarative and imperative programming is not always clear.

An SQL query is a representative example of declarative programming.

SELECT name
FROM users
WHERE active = TRUE
ORDER BY name;

This query does not specify in what order data files should be read, which indexes should be used, or which algorithms should perform sorting and filtering. The user describes the form of the desired result, while the query optimizer of the database management system determines the actual execution plan.

Declarative programs can make expressions concise by leaving the details of execution to the system and can allow the runtime to choose different optimization strategies depending on the situation. However, because the execution process is determined by the implementation, analyzing performance problems may require a separate understanding of internal execution plans and optimization rules.

Logic Programming

Logic programming represents a program as a collection of facts, rules, and queries. The programmer describes which relationships are true, and the execution engine uses logical inference to find solutions that satisfy a query.

Robert Kowalski described algorithms as a combination of logic and control. Logic represents the relationships and meanings that must hold in a problem, while control determines the order and method through which those relationships are explored. This distinction became an important foundation for separating the meaning of a problem from its execution strategy in logic programming.

Prolog is a representative logic programming language. A Prolog program defines facts and rules and then performs queries.

parent(alice, bob).
parent(bob, carol).

ancestor(X, Y) :-
    parent(X, Y).

ancestor(X, Y) :-
    parent(X, Z),
    ancestor(Z, Y).

Here, the ancestor rules define the relationship between parents and ancestors without expressing a specific sequence of computation through ordinary imperative statements. The execution engine uses unification and backtracking to search for variable values that satisfy the conditions.

Logic programming has been used in natural-language processing, knowledge representation, theorem proving, expert systems, program analysis, and rule-based systems. Restricted logic languages such as Datalog are also used for database queries, static analysis, access-control policies, and the representation of relationships in distributed systems.

Constraint Programming

Constraint programming is a paradigm in which the programmer describes conditions that variables must satisfy rather than writing a procedure that directly calculates their values, allowing a constraint solver to search for possible solutions.

For example, in a scheduling problem, the start and end times of each task can be represented as variables, and constraints such as the following can be defined.

  • Task A must be completed before task B.
  • Two tasks cannot use the same resource at the same time.
  • All tasks must end before the specified deadline.
  • A particular task may run only within an allowed time interval.

A constraint solver reduces the domains of possible values for each variable, propagates constraints, and, when necessary, performs a search that selects candidates and backtracks. Constraint programming is well suited to scheduling, resource allocation, combinatorial optimization, circuit design, configuration problems, and verification.

Constraint logic programming combines logic programming and constraint programming, allowing logical relationships and numerical, set-based, and temporal constraints to be expressed together within a single program.

Rule-Based Programming

Rule-based programming expresses actions to be performed when particular conditions hold as rules. A typical rule consists of a condition section and a conclusion or action section.

Condition:
    The user account is locked, and
    30 minutes have passed since the last authentication failure.

Action:
    Unlock the account.

A rule engine examines the current facts and state, finds applicable rules, and executes them according to priorities and conflict-resolution policies. Rule-based approaches are used in expert systems, business policies, access control, event processing, and game behavior systems.

Rule-based programming may appear similar to logic programming, but it does not necessarily use a formal logical inference model. Some rule systems include imperative actions that directly modify state, and the execution order of rules may affect the result.

Generic Programming

Generic programming is a paradigm for writing algorithms and data structures that are not dependent on particular data types or implementations and for specifying the operations and properties required by those algorithms.

For example, a sorting algorithm can be written not only for an array of integers, but so that it can be applied to multiple data types whose elements can be compared and exchanged. What the algorithm requires is not a specific type name, but the conditions that elements can be compared and moved.

Generic programming was systematized through the work of David Musser and Alexander Stepanov during the 1980s. The C++ Standard Template Library later provided a representative generic programming system in which iterators, containers, and algorithms were separated while remaining composable through type parameters and compile-time polymorphism. Generic programming also places importance on whether abstract algorithms can be specialized for concrete types without unnecessary runtime cost.

The methods used to express generic programming differ by language.

  • C++ uses templates and concepts.
  • Java and C# use generic types and constraints.
  • Rust uses generics and trait bounds.
  • Haskell uses parametric polymorphism and type classes.
  • Go uses type parameters and interface constraints.
  • ML-family languages use polymorphic functions and module systems.

Generic programming is not simply a technique for replacing type names in code with parameters. It is a design method that identifies concepts shared across multiple types and algorithms and expresses those concepts through the minimum necessary requirements.

Data-Flow Programming

Data-flow programming represents a program as a graph of operation nodes and the data dependencies connecting them. Each operation is executed when the required inputs become available, and its result is passed to the next connected operation.

A traditional imperative program executes commands in the order indicated by a program counter, while in a data-flow model the availability of data determines whether an operation can execute. Operations that do not depend on one another can run simultaneously, making the data-flow model a natural foundation for expressing parallel computation. Jack Dennis's research on data-flow architectures concretized a computational model in which operations are activated according to data dependencies rather than command order.

A data-flow program can be represented in the following form.

Input A ─┐
         ├─ Multiply ─┐
Input B ─┘            │
                      ├─ Add ─ Result
Input C ──────────────┘

The multiplication operation runs when both A and B are available, and the addition operation runs when the multiplication result and C are available. Program control flow is not specified separately, but is derived from the relationships of data dependence.

Data-flow approaches are widely used in graphical programming environments, digital signal processing, streaming systems, build systems, spreadsheets, GPU computation graphs, and machine-learning frameworks. However, representing iteration, conditional branching, and mutable state within a data-flow graph requires separate nodes and tokens, control dependencies, or state-management structures.

Event-Driven Programming

Event-driven programming is a model in which program execution flow is determined by events such as user input, network messages, timers, file changes, and sensor signals. A program registers event handlers or callbacks for particular events, and the execution environment invokes the appropriate handler when an event occurs.

Graphical user interfaces are representative event-driven systems. A program does not directly control when the user will press a button, but defines the action to be performed when a button-click event occurs.

button.addEventListener("click", () => {
    saveDocument();
});

Event-driven programs are suitable for servers, user interfaces, games, embedded systems, and distributed services that react to external input. However, when callbacks become nested across several levels and modify shared state, it can become difficult to understand execution order and error propagation. Promises, futures, coroutines, and async·await are used to express the linking of asynchronous events and the delivery of results in a more structured way.

Event-driven programming and reactive programming are closely related but are not the same concept. An event-driven program generally executes a registered handler imperatively when an event occurs. A reactive program places greater emphasis on declaring dependencies between values and events that change over time and arranging for changes to propagate through those relationships.

Reactive Programming

Reactive programming is a paradigm that represents values and streams of events occurring over time and automatically propagates changes in inputs to related computations. Continuous changes in values or asynchronous events are modeled as streams or signals, and new flows are created through operations such as filtering, transformation, and combination.

A spreadsheet can be regarded as a simple example of reactive computation. When the value of one cell changes, the results of formulas that refer to that cell are automatically recalculated. The programmer does not directly write the order of commands that update each cell, but defines the dependency relationships between values.

Functional reactive programming combines functional programming with reactive computation. Early FRP research expressed behaviors that change continuously over time and events that occur discontinuously as composable abstractions. FRP later developed into a method for declaratively constructing user interfaces, animations, robot control, and interactive systems.

Modern reactive libraries generally provide operations such as the following.

  • map transforms each event or value into another value.
  • filter allows only events that satisfy a condition to pass through.
  • merge combines multiple flows into one.
  • combine creates a new value based on multiple latest values.
  • debounce reduces repeated events within a short period into one.
  • buffer collects events over a particular period or count and processes them together.
  • subscribe connects a handler that receives the results of a flow.

Reactive programs must also manage backpressure when producers generate data faster than consumers can process it, as well as subscription lifetimes, error propagation, and execution schedulers.

Concurrent Programming

Concurrent programming is a paradigm for constructing programs in which two or more computations progress independently during the same period of time. Multiple tasks do not need to execute at exactly the same instant; concurrency exists even when they alternate on a single processor, as long as their lifetimes overlap.

Parallel programming focuses on executing two or more computations simultaneously in order to increase throughput or computation speed. Distributed programming deals with systems in which multiple computers with independent memory and failure domains cooperate through a network.

The three concepts are related but are not identical.

ConceptCentral problem
ConcurrencyHow to correctly organize and coordinate multiple tasks
ParallelismHow to execute computations simultaneously using multiple computing resources
DistributionHow to coordinate multiple systems in the presence of networks and partial failures

Concurrency models can be divided according to whether state is shared, how communication is performed, and how the execution order of tasks is defined.

Shared Memory and Threads

In a shared-memory model, multiple threads access values in the same memory. Communication between threads occurs through shared variables and data structures, while locks, semaphores, condition variables, and atomic operations are used to prevent data races when the same state is modified simultaneously.

Shared memory allows efficient communication because data does not need to be transferred directly between tasks, but program results may vary according to subtle differences in execution order. Avoiding deadlock, livelock, priority inversion, data races, and memory-visibility problems requires an understanding of the memory model defined by the language and execution environment.

Communicating Sequential Processes

Communicating Sequential Processes is a model in which independently executing sequential processes interact through explicit communication. In a 1978 paper, Tony Hoare proposed input/output and synchronized communication between processes as fundamental components of parallel programs.

In CSP, each process manages its own local state and exchanges values through channels. Communication serves not only as data transfer but also as synchronization between processes.

Go's goroutines and channels are representative programming models influenced by CSP. However, Go's execution model and channels do not implement the original formal CSP directly, but provide language features and runtime scheduling adapted to practical systems programming.

Actor Model

The Actor model represents a concurrent system as a collection of independent actors. When an actor receives a message, it can perform the following actions.

  • Change its own internal state.
  • Send messages to other actors.
  • Create new actors.
  • Determine how it will process the next message.

Actors do not directly access the internal state of other actors and communicate through asynchronous messages. The actor model originated in a model of concurrent computation proposed by Carl Hewitt, Peter Bishop, and Richard Steiger in 1973.

The process model of Erlang and Elixir, Akka, and several distributed actor frameworks were influenced by actor concepts. Isolated actor state and message passing reduce shared-memory contention and are well suited to representing components of distributed systems.

However, message-delivery delay, ordering, duplication, loss, and actor failure must still be handled, while consistent state changes spanning multiple actors require separate protocols. Using actors does not automatically solve the failure and consistency problems of distributed systems.

Coroutines and Structured Concurrency

A Coroutine is a computational unit that can suspend execution and later resume from the same location. It is used to implement generators, asynchronous functions, and cooperative tasks and is often provided as a lighter execution unit than an operating-system thread.

async and await express asynchronous operations in a structure similar to ordinary sequential code. A function can suspend while waiting for an input/output result and return control to an executor or event loop.

Structured concurrency is an approach that connects the lifetimes of concurrent tasks to the blocks and call structure of code. A parent task manages the completion, failure, and cancellation of its child tasks, reducing tasks that remain outside the program's visible structure and asynchronous execution that is difficult to track.

Coroutines do not guarantee parallelism by themselves. Multiple coroutines may execute cooperatively on a single thread, or they may run in parallel over multiple threads or task executors.

Aspect-Oriented Programming

Aspect-oriented programming is a paradigm that seeks to separate cross-cutting concerns that repeatedly appear across multiple modules and classes into independent units. Logging, authorization checks, transactions, performance measurement, and error recovery are representative cross-cutting concerns that are commonly inserted into many parts of a program.

In ordinary object-oriented or procedural structures, these features may be repeated across many functions and classes and become mixed with core business logic. Aspect-oriented programming defines cross-cutting concerns as aspects and applies their behavior at particular join points in a program. Research by Gregor Kiczales and others systematized aspect-oriented programming as a method for improving separation of concerns.

The main concepts are as follows.

  • A join point is a location in a program where additional behavior can be applied, such as a function call or object creation.
  • A pointcut specifies which join points are selected.
  • Advice is behavior executed before, after, or around a selected point.
  • Weaving is the process of combining aspect behavior with the target program.

Aspects can separate core logic from repeated auxiliary functionality, but because behavior not directly visible in the source code may be inserted from another location, the actual control flow can become difficult to trace. Modern languages and frameworks often provide similar functionality through attributes, annotations, decorators, proxies, middleware, and compiler plugins rather than through a complete aspect-oriented language.

Metaprogramming

Metaprogramming is a method of writing programs that analyze, generate, or transform other programs as data. It is often regarded not as an independent core paradigm, but as a program-construction technique that can be used across multiple paradigms.

Metaprogramming appears in forms such as the following.

  • Preprocessor macros replace or generate source code.
  • Syntactic macros transform abstract syntax structures.
  • Templates generate code based on types and constants.
  • Reflection inspects program types and structures at runtime.
  • Code generators create source code from schemas or interface definitions.
  • Compiler plugins analyze or modify declarations and expressions.
  • Partial evaluation precomputes known inputs and specializes a program.

Macros in LISP-family languages allow the syntactic structure of a program to be manipulated as the language's own data structures. C++ templates are used not only for generic programming but also for compile-time computation and code generation. Rust macros provide both pattern-based declarative macros and procedural macros that process syntax trees.

Metaprogramming can reduce repetitive code and add new abstractions to a language, but the generated code and actual execution flow may not be clearly visible in the original source. Therefore, error messages, debugging, tool support, build time, and code readability must be considered together.

Multi-Paradigm Programming

Modern general-purpose programming languages generally support multiple paradigms together. Even within a single application, different paradigms may be combined according to the nature of each problem.

For example, a game engine can be organized as follows.

  • Imperative programming is used for render-command generation and device control.
  • Object-oriented or data-oriented design is used for game systems and tool components.
  • Functional operations are used for collection transformations and asset processing.
  • Data-flow and concurrency models are used in the task scheduler.
  • Data-flow programming is used for shader graphs.
  • Event-driven and reactive programming are used for editor interfaces.
  • Metaprogramming is used for serialization and code generation.

The purpose of multi-paradigm programming is not to use every possible method within a single program. It is to select the model that expresses the structure of each problem most clearly and to establish clear boundaries between different models.

When paradigms are combined, ownership of state and data, error propagation, concurrency boundaries, call direction, and abstraction levels must be kept consistent. Functional transformations can be used within an object-oriented layer, but if they modify an object's internal state in unpredictable ways, the advantages of both models may be lost. When reactive streams are combined with imperative callbacks, the creation and disposal of subscriptions, execution order, and error-handling rules must likewise be clearly defined.

Paradigms and Software Design

A programming paradigm is not merely a question of selecting a particular syntax. It affects how a program is decomposed into units and how relationships between those units are defined.

In the imperative paradigm, state changes and command order are central. In the object-oriented paradigm, object responsibilities and collaboration are central. In the functional paradigm, value transformations and function composition are central. In logic and constraint paradigms, the relationships and conditions that must hold are central. In the data-flow paradigm, dependencies between operations are central, while in concurrency paradigms, communication and synchronization between independent tasks are central.

The same problem can be expressed differently depending on the paradigm. A task that transforms values satisfying a condition in a collection and then sums them can be viewed as follows.

Imperative:
    Iterate through the elements, check the condition,
    and update a variable.

Functional:
    Compose filter, map, and reduce.

Object-oriented:
    Ask a collection object to perform transformation and aggregation.

Logic:
    Describe the relationships and conditions of the elements to include in the result.

Data-flow:
    Connect filter, transformation, and aggregation nodes.

Parallel:
    Divide the input, compute across multiple tasks, and then combine the results.

No one expression is always superior. The appropriate method differs according to input size and frequency of change, performance requirements, error handling, team experience, existing code, and the execution environment.

Programming paradigms are also not stages in a linear progression in which one completely replaces another. Procedural languages did not disappear with the emergence of object-oriented languages, and functional programming has not completely replaced imperative programming. New paradigms allow problems that were difficult to express through existing methods to be approached from different perspectives, and they are absorbed into modern languages and software design in combination with existing paradigms.

Therefore, understanding paradigms has a broader meaning than memorizing language-specific syntax. It requires the ability to model the same computation in multiple ways and compare how state, control, data, abstraction, and concurrency are expressed in each approach. This understanding provides a foundation for selecting program structures and language features appropriate to a problem without being bound to a particular language.

Execution and Implementation

Source code written in a programming language is not itself a sequence of instructions that a central processing unit can execute. A language implementation reads the source code, analyzes its syntax and meaning, and then transforms the program into machine code, bytecode, an intermediate representation, or an internal structure that an interpreter can process. The transformed program is executed with the assistance of an operating-system loader, a virtual machine, or a language runtime.

This process differs significantly depending on the language and implementation. Typical implementations of C and C++ compile source code into object files and link them to produce native executable files. Java and Kotlin generally generate JVM bytecode, while C# and F# use the Common Intermediate Language of .NET. CPython transforms Python source code into code objects containing internal bytecode and executes them on a virtual machine. JavaScript engines often parse source code and then use an interpreter together with a JIT compiler. Therefore, the process from source code to execution is not a single fixed procedure, but an implementation system in which multiple stages can be selected and combined.

A traditional native compilation process can be summarized as follows.

Source code
    ↓
Preprocessing
    ↓
Lexical analysis
    ↓
Tokens
    ↓
Syntax analysis
    ↓
Abstract syntax tree
    ↓
Semantic analysis and type checking
    ↓
Intermediate representation
    ↓
Optimization
    ↓
Target code generation
    ↓
Assembly or machine code
    ↓
Object file
    ↓
Linking
    ↓
Executable file or library
    ↓
Operating-system loader and runtime
    ↓
Execution

Not every implementation clearly separates these stages in the same order. Some languages have no preprocessor, some implementations generate bytecode directly from an abstract syntax tree, and some compilers analyze source code during execution or divide intermediate representations into multiple stages. The Clang driver also conceptually distinguishes preprocessing, compilation, assembly, and linking, but in actual execution it may integrate some stages within a single process or connect them through pipes and temporary files.

Source Code and Translation Units

Source code is the original program text written by a programmer according to the syntax of a particular programming language. It generally consists of one or more files, each of which is processed together with its character encoding, line endings, file path, and module or package structure.

A compiler does not necessarily read an entire project at once. In C and C++, the traditional model of treating one preprocessed source file as a translation unit is widely used. Object files generated from multiple translation units are later combined by a linker into a single executable file or library. In Java, one class file generally contains the binary representation of one class or interface, and the JVM can load required classes individually during execution.

Modern language module systems do not necessarily treat source files and logical program units as identical. One module may consist of several files, while one file may contain multiple types and declarations. A compiler may analyze dependencies between modules to determine the processing order or store only public interfaces in separate intermediate files to reduce the cost of reanalyzing other modules.

Preprocessing

Preprocessing is a stage that transforms portions of source code before full syntax analysis begins. Not every language uses a separate preprocessor, but preprocessing directives and macros occupy an important position in the processing of C and C++.

A C-family preprocessor generally performs the following tasks.

  • Includes the contents of files specified by #include directives.
  • Expands macros defined with #define.
  • Includes or excludes portions of code according to conditional-compilation directives.
  • Replaces predefined macros supplied by the implementation.
  • Tracks logical source locations and diagnostic information.

The preprocessing result can be viewed as a sequence of tokens in which source files and headers that were originally separated have been combined and macros have been expanded. The compiler's parser and semantic analyzer then operate on this result. In practice, Clang can tightly integrate preprocessing and parsing internally, so it does not necessarily create a complete preprocessed output file on disk before executing the next stage.

Preprocessor macros transform tokens without understanding the ordinary syntactic structure of the language. As a result, the scope, type, and evaluation order of code generated by a macro may differ from expectations. Modern languages sometimes provide similar functionality through mechanisms that understand language structures, including modules, constant expressions, generics, attributes, syntactic macros, and compiler plugins, rather than through a preprocessor.

Lexical Analysis

Lexical analysis is the process of transforming the characters of source code into a sequence of Tokens to which the language can assign meaning. The component responsible for lexical analysis is called a lexer or tokenizer.

Consider the following source code.

int result = value + 10;

A lexer roughly divides it into the following tokens.

Keyword(int)
Identifier(result)
Operator(=)
Identifier(value)
Operator(+)
Integer literal(10)
Delimiter(;)

The lexer distinguishes identifiers, keywords, numeric and string literals, operators, delimiters, and comments. It may also interpret the character encoding of the source code and process escape sequences, line continuation, and the meaning of whitespace. In Python, increases and decreases in indentation are converted into INDENT and DEDENT tokens that represent block structure, so whitespace has grammatical meaning. The official Python documentation likewise explains that a lexical analyzer converts input into a token stream before the program is delivered to the parser.

A token may store not only its kind and original text, but also information such as the file name, line and column, and start and end positions. This location information is used for error messages, code navigation, refactoring, and the generation of debugging information.

Syntax Analysis

Syntax analysis is the process of determining whether a sequence of tokens conforms to the grammar of a language and constructing the structural relationships between those tokens. The component that performs this process is called a Parser.

For example, the following expression has a hierarchical structure determined by operator precedence.

a + b * c

A parser does not treat it merely as five tokens listed from left to right, but interprets it as a hierarchy such as the following.

Addition
├── a
└── Multiplication
    ├── b
    └── c

The rule that multiplication is performed before addition is reflected in the syntactic structure. Parentheses, operator associativity, function calls, conditionals, loops, declarations, and type expressions are likewise structured by the parser according to the grammar.

Parsers can be implemented using recursive-descent parsing, LR, LALR, LL, PEG, and other methods. Some parsers are generated automatically from grammar definitions, while others are written manually by implementation developers. Clang's parser receives tokens from the lexer, verifies the syntactic structure of the source, and generates an AST together with semantic analysis.

When source code does not conform to the grammar, the parser reports a syntax error. Rather than stopping immediately at the first error, a practical parser attempts to estimate where the error occurred and recover at the next statement or declaration so that multiple errors can be displayed in a single compilation. However, because the structure after an invalid token may be estimated incompletely, the first error may also cause several following diagnostics.

Abstract Syntax Tree

An Abstract syntax tree is an internal representation of the grammatical structure of a program in tree form. It is generally abbreviated as AST.

An AST differs from a concrete syntax tree that preserves every character of the original source. It omits some elements, such as parentheses and semicolons, that determine structure but are not directly required for execution meaning, and instead represents meaningful relationships among declarations, expressions, types, and statements.

Consider the following code.

int square(int value) {
    return value * value;
}

The AST may have approximately the following structure.

Function declaration: square
├── Return type: int
├── Parameters
│   └── value: int
└── Function body
    └── return
        └── Multiplication
            ├── Reference to value
            └── Reference to value

The AST is used as the foundation for type checking, name resolution, static analysis, code generation, refactoring, and code navigation. A compiler may add information such as resolved types, declaration references, source locations, constant values, and access permissions to AST nodes. Clang generates an AST as an abstract representation of the source from the results of parsing and semantic analysis and manages long-lived AST nodes, including types and declarations, through ASTContext.

Some development tools must preserve comments, whitespace, and exact token positions from the original source, so they retain a concrete syntax tree or token information separately from the AST. Automatic formatters and source-code transformation tools must handle not only execution meaning but also the original notation and comment positions.

Semantic Analysis

Syntactically valid code is not necessarily semantically valid. Semantic analysis examines an AST to determine whether the program satisfies the static semantic rules of the language and calculates information required by later stages.

Semantic analysis generally includes the following tasks.

  • Name resolution that connects names to declarations
  • Scope checking for variables, functions, and types
  • Determination of expression and declaration types
  • Checking implicit type conversions
  • Function-overload resolution
  • Constraint checking for generics or templates
  • Access-control checking
  • Checking inheritance and interface relationships
  • Validity checking for return statements and control flow
  • Constant-expression evaluation
  • Checking initialization order and lifetime rules
  • Diagnostics for structures prohibited by the language

For example, the following code is syntactically a valid function-call expression, but semantic analysis may report an error if the function's parameter permits only integers.

calculate("text")

The semantic analyzer compares the type of "text" with the parameter type of calculate and determines whether the call is valid. In a language such as C++ that supports overloading, it must select the function that best matches the argument types and conversion rules from among several functions with the same name.

Clang calls the component responsible for semantic analysis Sema. It determines whether source code is semantically valid and generates most compiler diagnostics. The parser and Sema operate closely together, allowing names and types to be interpreted while syntactic structures are being created.

An AST that has completed semantic analysis contains more information than a simple syntax tree. Identifiers are connected to actual declarations, expressions are assigned types, and implicit conversions and call targets are made concrete. Based on this information, the compiler can transform program behavior into a lower-level representation.

Static Analysis and Diagnostics

A compiler can analyze not only violations of language rules, but also code that can execute while still being likely to contain defects. Static analysis is a method of examining source code or an intermediate representation to find potential problems without directly executing the program.

Static analysis can detect issues such as the following.

  • Use of uninitialized values
  • Unreachable code
  • Conditions that are always true or always false
  • Possible use of freed memory or null references
  • Resource leaks
  • Possible out-of-bounds array access
  • Invalid lock ordering
  • Possible data races
  • Ignored return values
  • Mismatches between format strings and arguments
  • Operations that may cause undefined behavior

A static analyzer may use control-flow graphs, data-flow analysis, symbolic execution, abstract interpretation, and other techniques. Since it is generally impossible to predict every actual input and execution path completely, analysis results may include warnings that are not actual defects or paths that the analyzer could not prove safe.

Compiler diagnostics may be divided into errors, warnings, and notes. An error indicates a problem that prevents the creation of a valid program and requires compilation to stop, while a warning reports a suspicious structure even if it is permitted by the language. A good diagnostic system shows not only the location of the problem, but also related declarations and possible fixes.

Intermediate Representation

An Intermediate representation is an internal form that represents a program between the source language's AST and the instructions of the actual target machine. It is generally abbreviated as IR.

Compilers use intermediate representations to reduce the complexity that arises when directly connecting source languages to target architectures. Frontends for multiple languages can generate a common IR, common optimizers can process it, and multiple backends can generate code for their respective CPUs or virtual machines.

C ─────────┐
C++ ───────┤
Rust ──────┼─ Common intermediate representation ─┬─ x86-64
Fortran ───┤                                      ├─ AArch64
Swift ─────┘                                      ├─ RISC-V
                                                  └─ WebAssembly

There does not need to be only one intermediate representation. Modern compilers may use several IRs with different levels of abstraction in sequence.

  • A high-level IR preserves relatively many source-language structures, such as functions, objects, arrays, exceptions, and coroutines.
  • A mid-level IR represents control flow, data flow, and memory access in forms that are easy to analyze.
  • A low-level IR uses structures close to registers and machine operations, although it may not yet depend on a particular CPU.
  • A machine IR contains target-specific information for actual instruction selection and register allocation.

LLVM IR is a typed low-level intermediate representation designed primarily around static single-assignment form. It is used as a common code representation throughout LLVM's compilation strategy and serves as a boundary between frontends, optimizers, and backends.

Static Single-Assignment Form

Static single-assignment form is an intermediate-representation model in which a value is assigned to a variable name only once. It is abbreviated as SSA.

Consider the following imperative code.

x = 10
x = x + 1
x = x * 2

In SSA, each assignment is given a new name.

x1 = 10
x2 = x1 + 1
x3 = x2 * 2

This structure makes it clear where each value is defined and where it is used. The compiler can easily trace value flow and perform optimizations such as constant propagation, dead-code elimination, and common-subexpression elimination.

At points where control-flow paths merge, a structure corresponding to a φ function can be used to select one of the values arriving from multiple paths.

if condition:
    x1 = 10
else:
    x2 = 20

x3 = φ(x1, x2)

The actual form and transformation rules of LLVM IR are defined more strictly, and the compiler maintains the SSA structure during optimization or lowers it into target-specific machine representations.

Lowering

Lowering is the process of transforming high-level language structures into simpler and lower-level operations.

The following features of a high-level language may not correspond directly to a single target-machine instruction.

  • Objects and virtual methods
  • Closures
  • Pattern matching
  • Exception handling
  • Coroutines and async·await
  • String interpolation
  • Iterators and range-based loops
  • Object creation using garbage collection
  • Generic functions
  • Dynamic type operations

The compiler transforms these structures into more fundamental operations, such as function calls, branches, memory allocation, state machines, function pointers, jump tables, and runtime checks.

A coroutine may be transformed into a state machine that stores suspension points and local-variable state. A closure may be transformed into a combination of an environment object containing captured variables and a callable function. A virtual-method call may be lowered into a structure that finds a function address from an object's type information and performs an indirect call.

Lowering does not need to occur in a single step. It may proceed several times, from an IR close to the source language to a common high-level IR, then to a low-level IR and target-specific machine IR. Using staged IRs allows each optimization and transformation to use information at the level most appropriate to it.

Optimization

Compiler optimization is a transformation that improves properties such as execution time, code size, memory access, and power consumption while preserving the observable meaning of a program.

An optimizer analyzes what a program computes and determines whether the same result can be obtained with fewer operations or more efficient instructions. However, transformations must remain within the observable behavior permitted by the language, its memory model, floating-point rules, exceptions, and the scope of undefined behavior.

Representative optimizations include the following.

  • Constant folding evaluates operations whose values are known at compile time.
  • Constant propagation carries values determined to be constant to later use sites.
  • Dead-code elimination removes operations and unreachable code that do not affect the result.
  • Common-subexpression elimination reuses a previous result when the same computation is repeated.
  • Function inlining replaces a function call with the code of the function body.
  • Loop-invariant code motion moves operations that produce the same result on every iteration outside the loop.
  • Loop unrolling duplicates a loop body several times to reduce the number of branches.
  • Vectorization combines identical operations on multiple elements into SIMD instructions.
  • Escape analysis determines whether an object is passed outside a function or thread and optimizes its allocation location.
  • Scalar replacement decomposes a small object or aggregate value into individual values.
  • Tail-call optimization reuses the current call frame for certain function calls.
  • Branch simplification removes or combines unnecessary conditions and jumps.

Optimization does not end in a single stage. Since one transformation may create opportunities for another, multiple optimization passes may be repeated or executed in a predefined order. For example, function inlining removes a call boundary, which may enable constant propagation and dead-code elimination.

Alias Analysis

In languages that use pointers or references, it is important to determine whether two different expressions refer to the same memory location. This is called Alias analysis.

If a compiler cannot rule out the possibility that two pointers refer to the same value, it must assume that a write through one pointer may change the result read through the other. This restricts operation reordering, vectorization, and keeping values in registers.

Information from a language's type system, ownership rules, qualifiers such as restrict, and immutable references can reduce possible aliasing and provide more information for optimization. Conversely, arbitrary pointer operations and calls to external functions can make memory effects difficult to infer.

Profile-Guided Optimization

Profile-guided optimization is a method that uses information collected from actual program execution during compilation. It is abbreviated as PGO.

A typical process is as follows.

Build an instrumented program
    ↓
Run it with representative input
    ↓
Collect call frequencies and branch probabilities
    ↓
Recompile using the profile

The compiler can distinguish frequently executed functions and branches from rarely executed error paths and adjust inlining, code layout, branch ordering, and register use. If the profile does not accurately represent real usage patterns, the expected effect may be reduced or performance may worsen for certain workloads.

Link-Time Optimization

Link-time optimization is a method in which translation units are compiled separately and the intermediate representations of multiple modules are analyzed together during linking. It is abbreviated as LTO.

With ordinary separate compilation, the compiler processing one translation unit cannot know the body of a function defined in another file. With LTO, the linker or a linker plugin combines the IR of multiple modules or exchanges summary information, enabling cross-module inlining, elimination of unused global code, and whole-program analysis.

Processing all IR at once can consume substantial memory and time, so large programs may use distributed or thin LTO structures that rely on module summaries and parallel backends.

Target Code Generation

After optimization, the intermediate representation is transformed into instructions for a target processor or virtual machine. This process is called Code generation.

A native code generator performs tasks such as the following.

  • Selects target instructions for operations in the intermediate representation.
  • Determines addressing modes supported by the target processor.
  • Assigns virtual registers to physical registers.
  • Spills values to the stack or memory when registers are insufficient.
  • Adjusts instruction order for the target processor.
  • Places arguments and return values according to the calling convention.
  • Constructs stack frames.
  • Determines the locations of branches, constants, and global data.
  • Generates auxiliary information required for exception handling and debugging.

Instruction Selection

Instruction selection is the process of transforming IR operations into actual instructions for a target processor.

Different processors provide different instructions for the same operation. One processor may load a value from memory and perform an operation in a single instruction, while another may need to load the value into a register first and then use a separate arithmetic instruction.

The code generator selects an appropriate combination of instructions by considering target-instruction cost and available features. If the CPU supports SIMD, specialized bit operations, or cryptographic instructions, those instructions may be used instead of combinations of general operations.

Register Allocation

An intermediate representation can use more virtual registers than the actual processor provides. Register allocation is the process of assigning virtual values to a limited number of physical registers.

When the number of simultaneously live values exceeds the available registers, some values are temporarily stored in stack memory. This is called register spilling. Since register access is generally faster than memory access, it is important to keep frequently used values in registers and reduce unnecessary stores and loads.

The function calling convention defines which registers must be preserved by the caller and which must be preserved by the callee. The code generator assigns registers while following these rules.

Instruction Scheduling

Modern processors can overlap the execution of several instructions, but data dependencies between instructions and limitations of execution units affect performance. Instruction scheduling is the process of reordering instructions while preserving their meaning in order to reduce pipeline stalls and resource conflicts.

Even when the target CPU dynamically reorders instructions during execution, arranging code in an appropriate order can increase the range of instructions that the processor can execute in parallel.

Assembly and Object Files

When a code generator outputs assembly language in textual form, an assembler converts it into machine code. A compiler may also use an integrated assembler to generate an object file directly without creating an intermediate assembly file.

An Object file is not a finished executable file, but an intermediate binary containing machine code, data, symbols, relocation information, and various forms of metadata.

A typical object file contains information such as the following.

  • Executable machine code
  • Read-only constants
  • Initialized global data
  • Size information for uninitialized global data
  • Symbols that are exported or referenced
  • Relocation information for locations whose actual addresses have not yet been determined
  • Exception-handling and stack-unwinding information
  • Debugging information
  • Platform and architecture information

Depending on the platform, object and executable formats such as ELF, PE/COFF, and Mach-O are used. Each format divides code and data into sections or segments and provides information required by the linker and loader.

Symbols and Relocation

A Symbol represents the name and attributes of an entity that must be identified during linking, such as a function, global variable, or static data object. An object file records symbols that it defines and symbols that must be imported from elsewhere.

For example, one file may call an external function as follows.

extern int calculate(int value);

int run(void) {
    return calculate(10);
}

At the time this file is compiled separately, the final memory address of calculate may be unknown. The compiler and assembler leave a temporary value and relocation entry at the call site. The linker finds another object file or library that defines calculate and applies the actual address or the method for calculating it.

Relocation is the process of modifying address-dependent locations in code and data according to their final placement. There are several relocation methods, including absolute addresses, relative addresses based on the current instruction position, global offset tables, and procedure linkage tables.

Linker

A Linker combines multiple object files and libraries to produce an executable file or shared library.

A linker generally performs the following tasks.

  • Collects and arranges sections from input object files.
  • Connects symbol definitions and references with the same name.
  • Searches libraries for undefined symbols.
  • Diagnoses duplicate or conflicting definitions.
  • Determines the final addresses of code and data.
  • Applies relocations.
  • May remove unused sections.
  • Determines the order of initialization and termination functions.
  • Generates information used by the dynamic linker.
  • Records the executable entry point and program headers.

Static Linking

Static linking includes library code required by a program directly within the executable file. Since the library file does not need to be loaded separately at runtime, distribution can be simpler, but the executable becomes larger and multiple programs may each contain copies of the same library code.

A static library is generally an archive containing multiple object files. The linker selects objects that provide symbols actually referenced by the program and includes them in the result.

Dynamic Linking

Dynamic linking connects a program to an external shared library when the program is loaded or when a symbol is first used, rather than copying all library code and data into the executable.

On ELF-based systems, an executable participating in dynamic linking can specify a dynamic linker through program-interpreter information. The operating system loads the dynamic linker into the process address space together with the executable. The dynamic linker locates required shared objects, resolves symbols, and processes relocations.

Dynamic linking allows multiple processes to share read-only library code and permits a compatible library to be replaced without rebuilding the executable. However, the correct version of the library must exist in the execution environment, and symbol versions, search paths, and ABI compatibility must be managed.

ABI and Calling Conventions

An Application binary interface is a set of rules that allows separately compiled code to interact at the binary level. It is abbreviated as ABI.

An ABI generally includes matters such as the following.

  • How function arguments are placed in registers and on the stack
  • How return values are transferred
  • Which registers must be preserved before and after a call
  • Stack alignment and stack-frame structure
  • Sizes and alignment of primitive types
  • Memory layout of structures and classes
  • How symbol names are transformed into binary names
  • Exception-handling and stack-unwinding rules
  • Structure of virtual-function tables
  • Object-file and executable-file formats
  • System calls and operating-system interfaces

Even if source-level function declarations are identical, code using different ABIs may interpret arguments and return values differently and therefore cannot call each other correctly.

C++ uses Name mangling to encode information such as function overloading, classes, namespaces, and templates in symbols. The Itanium C++ ABI defines function-call interfaces, exception handling, global names, and object-code conventions so that independently compiled C++ code can operate together.

An ABI is not the same as a language specification. A language specification defines the meaning of a program, while an ABI defines how that meaning is represented in a binary structure on a particular platform and implementation. Even for the same language, ABIs may differ depending on the operating system, CPU architecture, and compiler family.

Executable Files and Loading

A Loader is an operating-system component that reads an executable file, constructs the address space of a new process, and begins execution.

A typical loading process is as follows.

  1. Inspect the header of the executable file.
  2. Confirm the target architecture and executable format.
  3. Map code, read-only data, and writable data into memory.
  4. Prepare the uninitialized-data region.
  5. Create the stack and initial process state.
  6. Load the dynamic linker if dynamic linking is required.
  7. Map shared libraries and apply relocations.
  8. Initialize the language runtime and global objects.
  9. Transfer control to the program entry point.

The entry point initially invoked by the operating system is not necessarily the main function written by the user. In C and C++ programs, startup code receives arguments and environmental information from the operating system, initializes the runtime, and then calls main. When the program terminates, it may pass the return value to the operating system and execute registered termination handlers and cleanup for static objects.

The Java Virtual Machine distinguishes class loading, which locates a binary class representation; linking, which incorporates the class into the execution state; and initialization, which prepares static state. The JVM can also load new classes during execution through user-defined class loaders.

Runtime System

A Runtime system is a software layer that supports programming-language features while a compiled program is running. The scale and role of a runtime differ significantly between languages.

A typical C execution environment may use a relatively limited runtime consisting of startup code, the standard library, stack-unwinding functions, and arithmetic helpers. Java and .NET use large managed runtimes containing virtual machines, JIT compilers, garbage collectors, type systems, class loaders, exception handling, and thread management.

A runtime can provide the following features.

  • Program and thread initialization
  • Management of function-call stacks and execution frames
  • Heap-memory allocation
  • Garbage collection
  • Management of object and type metadata
  • Dynamic method dispatch
  • Exception raising and stack unwinding
  • Reflection
  • Dynamic module and class loading
  • Thread and task scheduling
  • Coroutines and asynchronous input/output
  • Bounds checking for strings and arrays
  • Dynamic type operations
  • Interoperability with native code
  • Collection of profiling and JIT-optimization information

The .NET managed execution environment provides services such as JIT compilation, garbage collection, interoperability with unmanaged code, cross-language debugging, deployment, and version management.

Execution Stack and Call Frames

When a function is called, a Call frame is generally created to store the state required for that invocation. A call frame is also called a stack frame.

A call frame may contain the following information.

  • Return address
  • Function arguments
  • Local variables
  • Temporary computation results
  • Information pointing to the previous frame
  • Register values that must be preserved
  • Exception-handling and stack-unwinding information

When a function calls another function, a new frame is added, and when a function returns, that frame is removed. Recursive calls create separate frames for successive invocations of the same function.

The JVM gives each thread a private JVM stack and creates a frame containing a local-variable array, operand stack, and other information when a method is called. The JVM heap is shared by all JVM threads and stores class instances and arrays.

Not every value is necessarily stored in physical stack memory. In optimized native code, a local variable may exist only in a register or may be removed entirely. In virtual-machine implementations, call frames may be stored on a contiguous native stack, on the heap, or in a separate internal structure.

Heap and Memory Allocation

The Heap is a memory area used to store objects and data whose size and lifetime are determined dynamically during execution. Objects that must survive independently of the stack's call structure are generally allocated on the heap.

A heap allocator finds and returns a memory block of the requested size and reuses freed regions for later requests. An efficient allocator may use size-based free lists, region-based allocation, per-thread caches, and page-level management.

Heap use can cause the following problems.

  • Leaks caused by failing to release allocated memory
  • Access to memory that has already been released
  • Releasing the same memory twice
  • Fragmentation that reduces the amount of usable contiguous space
  • Synchronization costs caused by multiple threads accessing the allocator
  • Reduced cache efficiency because related objects are placed far apart

Languages and runtimes manage the lifetimes of heap objects through several methods, including manual deallocation, reference counting, tracing garbage collection, region-based management, and ownership analysis.

Garbage Collection

Garbage collection is a method in which the runtime automatically reclaims memory occupied by objects that can no longer be reached by a program.

A tracing garbage collector generally identifies a root set beginning with global variables, thread stacks, CPU registers, and other sources, then follows references from those roots to mark reachable objects. Unreachable objects can no longer be used by the program and therefore become candidates for reclamation. The .NET garbage collector likewise examines the graph of reachable objects based on root information managed by the JIT compiler and runtime.

Garbage collection can be implemented in several ways.

  • Mark-and-sweep collection marks reachable objects and then reclaims unmarked objects.
  • Copying collection moves live objects into another region and clears the original region at once.
  • Generational collection manages newly created objects and long-lived objects in separate regions.
  • Incremental collection divides the collection process into several smaller stages.
  • Concurrent collection executes portions of collection work at the same time as application threads.
  • Compacting collection gathers live objects together to reduce fragmentation.

Garbage collection automates memory deallocation, but it does not mean that external resources requiring immediate release, such as files, sockets, locks, and GPU objects, are automatically cleaned up at an appropriate time. Such resources may use separate explicit cleanup statements, scope-based management, and termination methods.

Exception Handling

Exception handling is a control structure that transfers errors or special conditions that are difficult to process through normal return flow to an upper-level caller.

When an exception occurs, the runtime searches for an appropriate handler in the current function. If none exists, it moves to the frame of the calling function and continues moving upward through the call stack until a handler is found. During this process, destructors or cleanup statements for previously created local objects may be executed. This is called Stack unwinding.

Depending on the implementation, exception handling can be organized in the following ways.

  • Continuously checking whether an exception occurred during every function call and operation
  • Avoiding additional branches during normal execution and using tables to locate handlers only when an exception occurs
  • Allowing the runtime to manage exception objects and type information
  • Combining language exceptions with an operating system's structured exception handling or signal system

A C++ ABI can define binary rules for exception-handling interfaces, stack unwinding, object cleanup, and handler lookup. Passing exceptions between different compilers or language runtimes requires a compatible ABI and exception model.

Exceptions are a language-level method of representing errors and are not always identical to hardware exceptions raised by the operating system. Some implementations convert hardware or operating-system faults, such as division by zero and invalid memory access, into language exceptions, while others immediately terminate the program.

Bytecode and Virtual Machines

Bytecode is an instruction format targeting a virtual machine rather than a particular physical CPU. Transforming source code into bytecode places a common execution layer between the language frontend and actual hardware.

A virtual machine defines bytecode instructions, a memory model, calling rules, type verification, and a module format. Bytecode may be executed directly by an interpreter or transformed into native code by a JIT compiler.

JVM

Java source code is generally compiled into a class format containing JVM instructions. The JVM does not directly understand the Java language itself, and other languages capable of producing a valid class format can also execute on the JVM. A JVM class file includes bytecode, a constant pool that serves as a symbol table, fields, methods, and various attributes.

Before executing a class file, the JVM can verify the validity of its structure and bytecode. The verification process checks that instruction type relationships, stack use, branch targets, and other elements satisfy JVM rules, limiting the ability of invalid binaries to break the runtime's internal constraints.

CPython

CPython transforms Python source code into internal code objects and bytecode. A code object is an immutable object containing compiled executable code and related metadata, while a function object combines a code object with execution context such as a global namespace and default arguments.

When importing a module, CPython can store the compilation result in a .pyc cache file. On later executions, if the cache matches the current source, the work of converting the source into bytecode again can be reduced. Python's import system can validate the cache using timestamps and file sizes or source hashes.

.NET

.NET language compilers generally generate assemblies containing Common Intermediate Language and metadata. The CLR loads an assembly, interprets type and method information, and can JIT-compile methods to be executed or use previously compiled code.

The common type system and metadata allow different languages such as C# and F# to use the same runtime and libraries. However, not every unique feature of each language is exposed in the same form to other languages, and public interfaces that can be expressed through common rules must be used for interoperability.

WebAssembly

WebAssembly is a portable low-level virtual instruction format. Its core specification defines an instruction set, types, modules, binary encoding, validation, and execution semantics. Methods of interacting with a particular operating system or browser API are separated from core execution semantics and provided through a host or separate interface layer.

A WebAssembly module is validated according to structural and type rules before execution. Instantiating a module creates runtime instances of functions, memory, tables, global variables, and other elements, initializes data segments, and may execute a designated start function.

Interpreter

An Interpreter reads an internal representation of a program and directly performs operations corresponding to its meaning.

The simplest tree-walking interpreter recursively visits AST nodes.

Execute an addition node:
    Evaluate the left child node.
    Evaluate the right child node.
    Add the two results.

Tree walking is easy to implement and provides a clear correspondence between source structure and execution. However, it can have significant execution costs because each operation requires identifying the node type and following pointers.

A bytecode interpreter transforms an AST into a denser array of instructions and executes them while advancing a virtual program counter.

LOAD_CONST 10
LOAD_LOCAL value
ADD
RETURN

A bytecode interpreter uses instruction dispatch and an operand stack or virtual registers. A virtual machine may use a stack-based or register-based structure, each of which has different properties in instruction size, decoding cost, and optimization potential.

Interpreters have short compilation times and can flexibly modify program structures during execution. However, because the cost of interpreting each operation is repeated, they may be slower than native code for long-running computations. A modern runtime may begin execution quickly with an interpreter and then JIT-compile only frequently executed sections.

JIT Compilation

JIT compilation is a method of transforming bytecode or an intermediate representation into machine code for the current system while the program is running.

A JIT compiler can collect information such as the following during execution.

  • Functions that are frequently called in practice
  • The number of loop iterations
  • Actual ratios of conditional branches
  • Object types observed at a particular call site
  • Actual targets called by virtual methods
  • Array sizes and ranges of values
  • Paths on which exceptions frequently occur
  • Kinds of values repeatedly passed as function arguments

Using this information, code can be specialized based on conditions that an ahead-of-time compiler would find difficult to assume safely. For example, if only one object type has been observed at a particular call site, the runtime can optimize the call by omitting virtual-method lookup and directly invoking the corresponding function.

However, a new type may appear during execution and invalidate the existing assumption. In that case, the runtime must discard the optimized code and return to more general code or interpreter state. This is called Deoptimization.

JIT compilation also has the following costs.

  • CPU time is used for compilation during execution.
  • Memory is required to store generated machine code.
  • Sufficient profile information is unavailable immediately after program startup.
  • Performance may differ before and after optimization is completed.
  • Dynamically generating executable memory can be affected by security policies and code-signing requirements.

LLVM's ORC JIT API is designed to support JIT-based systems that transform LLVM IR into native code during execution and manage symbols.

Tiered Compilation

Tiered compilation uses multiple execution tiers with different execution speeds and compilation costs.

Fast interpreter
    ↓ Frequently executed
Low-cost JIT compilation
    ↓ Executed very frequently
High-cost optimizing JIT compilation

A program first begins execution immediately through an interpreter or fast compiler. The runtime observes execution frequency and recompiles frequently executed code at a higher optimization level. Code that executes rarely does not undergo expensive optimization, reducing startup time and memory use.

AOT Compilation

AOT compilation is a method of transforming a program into target code before execution. This may include not only ordinary native compilation, but also transforming a virtual-machine intermediate form into a native image before distribution.

The major characteristics of AOT compilation are as follows.

  • There is little or no compilation cost during execution.
  • Startup time and initial performance are easier to predict.
  • Dynamic code generation does not need to be permitted in the execution environment.
  • Code must be generated in advance for the target architecture.
  • Type and call information available only during execution can be used only to a limited extent.
  • Additional information may be required to preserve reflection, dynamic loading, and runtime code-generation features.

Modern platforms do not always choose only one of AOT and JIT. They may combine precompiled base code with profile-guided optimization during execution.

Native Interoperability

A Foreign function interface is a system for calling functions and exchanging data between different programming languages and runtimes. It is abbreviated as FFI.

An FFI must address the following issues.

  • How the types of two languages correspond to each other
  • Function calling conventions
  • String encoding and memory representation
  • Layout of structures and arrays
  • Lifetimes of objects and references
  • Memory ownership and responsibility for deallocation
  • Passing callback functions
  • Conversion of exceptions and errors
  • Connection of threads and runtime state
  • Movement and pinning of objects managed by a garbage collector

The C ABI is relatively simple and supported by many languages, so it is widely used as a common interoperability boundary. C++ classes, templates, exceptions, and standard-library objects depend more strongly on ABI and compiler version, so external interfaces often use the C calling convention, opaque handles, and explicit creation and destruction functions.

When a managed runtime calls a native function, a marshaling process may be required to convert managed objects into native-memory representations. Conversely, when native code references a managed object, a separate handle is needed so that the garbage collector recognizes the reference and preserves the object's lifetime.

Cross-Compilation

Cross-compilation is a compilation method in which the host system on which the compiler runs differs from the target system on which the generated program will execute.

For example, a program for AArch64 Linux or an embedded operating system can be built on an x86-64 Linux computer.

A cross-compilation environment generally requires the following elements.

  • A code-generation backend supporting the target architecture
  • The target ABI and calling conventions
  • Headers for the target operating system
  • The target standard library and system libraries
  • A target assembler and linker, or tools with those functions built in
  • A sysroot representing the target file system
  • A real device or emulator for execution and testing

It is not sufficient for the compiler merely to generate machine code for the target CPU. The libraries, startup code, and linker settings used by the program must also match the target environment. Clang can generate code for another architecture by specifying the target, CPU, ABI, sysroot, and header and library paths.

Heterogeneous Compilation

Modern programs may target more than one CPU. When different execution devices such as GPUs, DSPs, and neural-network accelerators are used together, host code and device code may need to be compiled separately from one source or module.

One source
├── CPU host code → x86-64 or AArch64
└── GPU device code → GPU ISA or SPIR-V

A toolchain may compile the same translation unit for multiple targets and include the generated device code within a host executable or package it as a separate binary. Clang's offloading toolchain can also create separate code objects for the host and one or more devices and combine them into one bundle.

Debugging Information

Optimized machine code does not preserve the variables, statements, and type structures of the original source code exactly. Debugging information is metadata that allows a debugger to reconnect machine-code execution state to source-level concepts.

Debugging information may include the following.

  • Correspondence between machine-code addresses and source files, lines, and columns
  • Function and inlined-call information
  • Types of variables and parameters
  • Registers or memory locations storing values at particular execution points
  • Field layouts of structures and classes
  • Namespaces and modules
  • Call-frame restoration information

DWARF is widely used in ELF environments on Unix-like systems, while CodeView and PDB-family information are used in Windows PE/COFF environments. DWARF is standardized as an information format for supporting source-level debugging across multiple compiled languages.

In an optimized program, values corresponding to source variables may be removed or moved between several locations, functions may be inlined, and statement execution order may change. As a result, the execution location and variable values shown by a debugger may not correspond perfectly to the intuitive order of the source.

Incremental Compilation and Caching

Compiling a large program entirely from the beginning every time increases development time. Incremental compilation reprocesses only changed parts and dependent elements affected by those changes.

An incremental compiler may store the following information.

  • Source hashes for each module
  • Public interfaces and type information
  • Dependencies between modules
  • Previous parsing and semantic-analysis results
  • Generated intermediate representations
  • Optimization results and object files
  • Compilation options and target environment

If only the implementation inside a function changes while its public interface remains the same, only the module containing that function may need to be regenerated. Conversely, when a public type, template, or inline function changes, other modules that reference it may also need to be recompiled.

A build cache uses source contents, compiler versions, options, headers, and dependency files as keys to reuse previous results. If cache keys are not constructed accurately, outdated results may be used incorrectly or the cache may be invalidated more often than necessary.

Bootstrapping and Self-Hosting

A compiler for a programming language is called a Self-hosting compiler when it is written in the same language that it compiles. Since a new language does not yet have its own compiler, it may initially be implemented in another language or begin with a small compiler that processes a subset of the new language.

A typical bootstrapping process is as follows.

Write an initial compiler in an existing language
    ↓
Compile the new language with the initial compiler
    ↓
Rewrite the compiler in the new language
    ↓
Build the new compiler with the previous compiler
    ↓
The new compiler compiles its own source

Self-hosting demonstrates that a language can be used to write real large-scale software and allows compiler developers to use their own language and toolchain directly. However, it introduces a bootstrap-trust problem: the compiler binary and source must be verified as trustworthy, and the influence of previous build stages on the result must be examined.

Conformance Between Language Specifications and Implementations

A language implementation must correctly implement the program meaning defined by the language specification. Even if a compiler produces faster code, it must not arbitrarily change the observable behavior of the program.

Implementation correctness can be checked using the following methods.

  • Conformance tests that verify each feature of the language standard
  • Unit and integration tests for the compiler itself
  • Differential testing that executes the same program on multiple implementations
  • Fuzzing that generates random programs
  • Verification of intermediate representations and optimization passes
  • Comparison of program behavior before and after compilation
  • Use of static analyzers and runtime checkers
  • Use of formally verified compilers or verified transformations

In languages that permit undefined behavior or implementation-defined behavior, every implementation does not need to produce the same result. In such cases, the specification distinguishes the scope that implementations must follow from the scope in which they may choose their behavior.

Binaries intended for virtual machines may undergo a separate validation stage before execution. WebAssembly verifies that instructions and modules satisfy static typing and structural rules, while the JVM likewise applies structural constraints to class files and bytecode.

Complete Execution Flow

Although the execution process of a programming language differs depending on the language and implementation, the role of each stage can be summarized as follows.

StagePrimary roleRepresentative result
PreprocessingProcesses included files, macros, and conditional codePreprocessed tokens
Lexical analysisSeparates characters into meaningful unitsTokens
Syntax analysisOrganizes tokens into grammatical structuresSyntax tree or AST
Semantic analysisChecks names, types, scopes, and language rulesAST with semantic information
LoweringTransforms high-level features into primitive operationsLow-level AST or IR
Intermediate-representation generationConstructs a form suitable for optimization and target translationIR
OptimizationImproves code while preserving meaningOptimized IR
Code generationDetermines target instructions, registers, and calling conventionsAssembly or machine code
AssemblyConstructs machine code and metadataObject file
LinkingResolves symbols, applies relocation, and combines binariesExecutable file or library
LoadingPlaces the program and libraries into memoryProcess execution state
RuntimeSupports language features such as memory, types, exceptions, and threadsRunning program

In implementations using bytecode and virtual machines, some stages differ as follows.

Source code
    ↓
Frontend analysis
    ↓
Bytecode or common intermediate language
    ↓
Validation and module loading
    ↓
Interpreter or JIT compiler
    ↓
Native instruction execution

Implementing a programming language is not simply a translation task that converts syntax into machine code. Compilers and interpreters must implement the meaning, type system, memory model, exception rules, and concurrency rules of a language on actual hardware and operating systems. Linkers, loaders, ABIs, runtimes, and standard libraries are also parts of the language ecosystem that determine program execution results.

Modern implementations combine AOT compilation, interpretation, bytecode, and JIT compilation as needed and optimize programs using multiple levels of intermediate representation and profile information. Therefore, the actual performance, memory use, startup time, portability, and debugging characteristics of a programming language are not determined solely by its syntax, but by the entire execution system formed by the compiler, runtime, operating system, ABI, libraries, and target hardware.

Type Systems

A Type system is a system of rules that classifies the values and expressions used in a program into defined categories and specifies the operations permitted for each category and the compatibility relationships between values. A type represents not only the format in which a value is stored in memory, but also the operations that can be performed on that value, the conditions under which it can be passed to another value or context, the inputs and outputs of functions, and the abstraction boundaries of a program.

For example, integers and strings are both values stored in memory, but the operations permitted on them and their meanings differ. Arithmetic operations can be applied to integers, while operations such as concatenation and length calculation can be applied to strings. A function type indicates what values a function accepts as input and what value it returns, while an object type may indicate the fields and methods that can be used or the interfaces that the object satisfies.

A type system is not a mechanism that eliminates every error in a program. Problems that are difficult to determine through types alone, such as a missing file, a failed network connection, invalid user input, or a logical error in an algorithm, may still occur during execution. The role of a type system is to detect mismatches between values and operations within the scope defined by the language, make the structure and intent of a program explicit, and provide compilers and development tools with information that can be used to analyze the program. Cardelli and Wegner treated types as a central concept for systematizing the classification, abstraction, and polymorphism of program expressions and values.

The main classifications of type systems can be summarized as follows.

CriterionMain classificationsWhat is distinguished
Checking timeStatic typing, dynamic typingWhether type rules are checked before or during execution
Type notationExplicit typing, type inferenceHow programmers and compilers determine type information
Type relationshipsNominal typing, structural typingWhether type compatibility is determined by names or structures
PolymorphismParametric, subtype, ad hoc polymorphismHow one piece of code handles multiple types
Value compositionProduct types, sum types, function typesHow composite values are represented
Resource usageLinear types, affine types, ownership typesHow many times a value can be used and who manages it
PrecisionOrdinary types, refinement types, dependent typesHow specifically conditions about values are included in types
EffectsOrdinary types, effect typesWhether a function's external effects are tracked through types
Adoption modelFully static typing, gradual typingTo what extent static type information is applied to a program

These classifications are not mutually exclusive. A language may provide static typing, type inference, nominal typing, generics, nullability analysis, and ownership checking together. Conversely, a dynamically typed language may add optional type annotations and a static analyzer to apply type checking to portions of the code.

Types and Typing Judgments

Type checking is the process of assigning a particular type to a program expression and verifying that the type is compatible with the context in which it is used. Formal type systems use typing judgments similar to the following.

Γ ⊢ expression : Type

This means that, under environment Γ, expression has the type Type. The environment contains information about variables, functions, type parameters, and their types that are available in the current scope.

For example, consider the following code.

x: Integer
y: Integer

x + y

The type checker applies the rules that x and y are integers and that the result of integer addition is also an integer, assigning the integer type to x + y. Conversely, attempting to add values such as a string and a file object, for which no addition rule is defined, produces a type error.

Function calls are checked in the same way.

parse: String → Integer

If parse is a function that accepts a string and returns an integer, a string expression can be passed as an argument, but passing an arbitrary object that cannot be converted to a string may not be permitted.

A typing judgment is not simply a comparison of whether type names are identical. Subtype relationships, implicit conversions, generic constraints, overload resolution, lifetimes, nullability, and the inferred results of type parameters may all be considered together.

Type Safety and Soundness

Type safety is the property of restricting a program from using values in ways prohibited by the type rules. For example, a type system may prevent an integer value from being invoked as a function pointer, a string from being interpreted as an arbitrary object layout, or a nonexistent method from being called.

In formal language theory, Type soundness is often explained through two properties known as preservation and progress.

  • Preservation is the property that a well-typed expression continues to satisfy the type rules after taking one execution step.
  • Progress is the property that a well-typed expression is either already a resulting value or can proceed to the next execution step defined by the language.

Proving these two properties together provides a basis for showing that a program accepted by the type checker will not enter a state during execution that is undefined by the type system. Wright and Felleisen systematized a syntactic approach to proving type soundness in this manner.

Type soundness is not the same as memory safety. If a language permits unsafe pointer operations, arbitrary type conversions, or calls to external native code, memory errors may occur even in code that passes static type checking. Some languages place such operations within a separate boundary, such as an unsafe region, clearly distinguishing the guarantees maintained by safe code. Rust can also use unsafe code inside safe abstractions, but the author of that code must directly guarantee pointer, aliasing, and lifetime rules.

What a type system guarantees differs between languages. Some languages check array bounds through types, while others check them during execution. Some treat integer overflow as an error, while others wrap the value. Therefore, the expression “type-safe language” should not be interpreted as guaranteeing every aspect of execution safety.

Static and Dynamic Typing

A Static typing system analyzes type relationships among variables, expressions, function calls, and other elements before the program is executed. In Java, the types of all variables and expressions are determined at compile time, and types restrict the range of values and permitted operations.

A static type checker can detect problems such as the following before execution.

  • Use of nonexistent fields or methods
  • Mismatches between function arguments and parameter types
  • Invalid return types
  • Disallowed type conversions
  • Violations of generic type constraints
  • Some unhandled patterns and type branches
  • Violations of nullability rules
  • Invalid reference lifetimes and ownership usage
  • Violations of certain effect and exception rules

Static typing does not mean that the actual value of every part of the program is completely known before execution. The type checker may know that a variable has an integer type, but it generally cannot know in advance whether the actual value is 1 or 100. A type abstractly represents a set of possible values and the range of permitted operations.

In a Dynamic typing system, the type of a value and the validity of operations are primarily checked during execution. Rather than assigning one fixed type to the variable name itself, it is more accurate to say that the value currently referenced by the name carries type information.

value = 10
value = "text"

In a dynamically typed language, the same name can refer to an integer and a string at different times. However, the distinction between integers and strings does not disappear. The runtime checks the type of the current value to determine whether a particular operation is permitted.

Dynamic typing is suitable for writing programs quickly and flexibly handling structures created during execution and data of varying forms. However, a particular type error may not be discovered until the relevant execution path is actually taken. Tests, runtime checks, optional type annotations, and external static analyzers are used together to compensate for this limitation.

Statically typed languages may also perform type checks during execution. Downcasting, dynamic module loading, reflection, interface objects, and deserialization of external data require inspection of actual runtime values. Conversely, a compiler for a dynamically typed language may diagnose obvious errors in advance or infer types for optimization. Therefore, static and dynamic typing do not mean that all checks exist only at one point in time, but indicate when the language places the primary responsibility for type correctness.

Explicit Types and Type Inference

A Type annotation is a type written directly by the programmer for a variable, function, parameter, return value, or another language element.

fn add(a: i32, b: i32) -> i32 {
    a + b
}

Type inference is the process by which the compiler determines types for portions of source code where they are not explicitly written by analyzing surrounding expressions and usage relationships.

let value = 10;

In this code, the compiler can infer the type of value based on the integer literal and its usage context. Using type inference does not make a language dynamically typed. The inferred type is used for static type checking, and assigning an incompatible value to the variable later may produce an error.

Type inference uses information such as the following.

  • Default types of literals
  • Operand types required by operators
  • Function parameter and return types
  • Assignment relationships between variables
  • Constraints on generic type parameters
  • Subtype and conversion relationships
  • Target types required by the surrounding context
  • Type narrowing based on control flow

Hindley–Milner-family type systems provided a framework capable of inferring the most general principal type even for functional programs with few explicit type annotations. The work of Damas and Milner established a principal-type system in which other types applicable to a particular program can be expressed as instances of the principal type.

Type inference in modern languages is not completely identical to Hindley–Milner inference. When features such as subtyping, overloading, objects, generics, lifetimes, and mutability are combined, more complex constraints are required. Java's type inference also converts expression compatibility and subtype relationships into constraints and then calculates possible bounds for type variables.

Type inference can have the following scopes.

  • Local type inference infers only some types within a function or expression.
  • Global type inference analyzes a broad portion of a program across function boundaries.
  • Unidirectional inference calculates a type from the expression itself.
  • Contextual inference passes a type required by the surrounding context into an expression.
  • Bidirectional type checking synthesizes the types of some expressions and checks whether other expressions match a given target type.

Bidirectional type checking distinguishes type synthesis from type checking and is used to control the amount of required type annotation and the complexity of checking even in highly expressive type systems.

Automatically inferring every type is not always desirable. Explicitly writing the types of public functions clarifies API contracts and error messages and reduces the possibility that an implementation change unintentionally alters an inferred result.

Primitive and Composite Types

Programming languages provide various types to represent kinds of values. Although the classification differs by language, the following types are commonly found.

  • Integers and real numbers
  • Boolean values
  • Characters and strings
  • Pointers and references
  • Arrays and slices
  • Tuples and records
  • Structures and classes
  • Enumeration types
  • Function types
  • Interfaces and traits
  • Generic types
  • Union and intersection types
  • Recursive types
  • Nullable types
  • Type parameters

Java divides types into primitive types and reference types. Primitive types include Boolean and numeric types, while reference types include classes, interfaces, and array types.

These classifications do not apply identically to every language. In some languages, integers are objects, while in others integers and objects have different memory representations and calling conventions. The difference between value types and reference types is also not defined simply as the difference between the stack and the heap. The core distinction lies in how values are copied, compared, and shared and how the language defines their identity.

Product Types

A Product type is a composite type that contains multiple values simultaneously. Tuples, records, and structures are representative examples.

Point = {
    x: Float,
    y: Float
}

A Point value must have both x and y. It is called a product type because every possible combination of an x value and a y value forms the complete set of Point values.

A tuple generally distinguishes components by position, while a record distinguishes them by name.

(Integer, String)
{
    id: Integer,
    name: String
}

A Haskell tuple is also defined as an algebraic data type with a single constructor.

Sum Types

A Sum type is a type whose value has one of several possible forms. It is also called a variant, tagged union, or discriminated union.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

A Result<T, E> value does not contain both Ok and Err simultaneously, but has one of the two forms. Each form may contain different data, and a tag or constructor distinguishes the current form.

Sum types are useful for explicitly representing states such as the following.

  • Success or failure
  • Presence or absence of a value
  • Multiple kinds of commands
  • Different syntax-tree node types
  • Network-message types
  • The current state of a state machine

Rust's enum defines a nominal enumeration type and its variants so that they can be constructed and used in pattern matching.

Algebraic Data Types

An Algebraic data type is a data type defined by combining product types and sum types. Haskell provides user-defined algebraic data types and pattern matching as core language features.

The following is a recursive algebraic data type representing a binary tree.

data Tree a
    = Empty
    | Node (Tree a) a (Tree a)

Tree a is either Empty or a Node containing a left tree, a value, and a right tree. Because the type definition refers to itself, it is also a recursive type.

Pattern matching checks the current constructor and extracts the values contained by that constructor.

size Empty = 0
size (Node left _ right) =
    1 + size left + size right

A compiler can check whether all forms of a closed sum type have been handled. This allows it to find code affected by the addition of a new variant or diagnose an unhandled state.

Function Types

A Function type represents the inputs accepted by a function and the output it returns.

String → Integer

This is the type of a function that accepts a string and returns an integer.

Multiple parameters can be regarded as a tuple.

(Integer, Integer) → Integer

In a language that uses currying, this can be expressed as a function that accepts one argument and returns another function.

Integer → Integer → Integer

In languages supporting first-class functions, function types can be used for variables, fields, function parameters, and return types.

In function-type subtyping, inputs and outputs vary in different directions. It may be safe to provide a more specific return type, but the parameter type must be broad enough to accept the values that the caller may pass. This relationship leads to covariance and contravariance.

Union and Intersection Types

A Union type indicates that a value may belong to one of several types.

string | number

A value of this type is either a string or a number. Its actual type must be narrowed through an execution path or condition check, and until the type is identified, only operations safe for all constituent types can be used. TypeScript uses control flow and type checks to narrow a union type into a more specific type.

An Intersection type is a type that simultaneously satisfies the conditions of multiple types.

Serializable & Loggable

This type must provide all the members required by both Serializable and Loggable. TypeScript intersection types combine the members of multiple types into one type.

Union and intersection types can be understood from the perspective of sets of values.

  • A | B is the set of values belonging to A or B.
  • A & B is the set of values satisfying the conditions of both A and B.

Special Types

A type system may contain special types that represent computational structures in addition to ordinary data types.

Top Type

A Top type is the broadest type containing every value in the type system. Depending on the language, it may be named Object, Any, unknown, or something similar.

Operations that exist only on a concrete type cannot be applied directly to a value of the top type. To use the value, it must be narrowed to a more specific type through type checks, pattern matching, or downcasting.

In some languages, any has stronger bypass capabilities than an ordinary top type. It may disable type checking or permit most operations, so it should not be treated as equivalent to the type-theoretic top type.

Bottom Type

A Bottom type is a type with no possible values. It can represent a function that never returns normally or always raises an exception.

Rust's ! type has no values and represents a result whose computation never completes. It can be coerced into another type.

fn terminate() -> ! {
    panic!("terminated");
}

Because the bottom type never actually produces a value, it can logically appear in a position requiring many different types.

let value = if condition {
    10
} else {
    terminate()
};

Because terminate() does not return a value, it does not conflict with the integer type of the other branch.

Unit Type

A Unit type is a type with exactly one value that contains no meaningful information. It can express that a function returns no computational result and only performs side effects.

Unit

A unit type differs from a bottom type, which represents the absence of values.

  • A unit type has one value.
  • A bottom type has no values.

Null Type

In some languages, null or an equivalent value is included in reference types. Other languages distinguish nullability within the type itself.

When nullability is implicitly included in ordinary reference types, every use of a reference must consider whether it is null. To reduce this burden, modern languages distinguish the presence or absence of a value through types such as T and T?, Option<T>, or Maybe T.

Nominal and Structural Typing

A Nominal typing system determines compatibility according to the declared names of types and explicit relationships between them.

class UserId
class OrderId

Even if both types have the same internal representation of a single integer, separately declared types can be treated as different. For one type to become a subtype of another, an explicit relationship such as inheritance or interface implementation is required.

Nominal typing is useful for distinguishing domain concepts and preventing unintended mixing of types. Even if length and mass use the same floating-point representation, defining them as separate types can prevent invalid assignments.

A Structural typing system determines compatibility based on the structures actually provided, such as fields and methods, rather than the names of types.

type Named = {
    name: string;
};

A value with a name: string field may be compatible with Named even if it does not explicitly declare that it implements the type. TypeScript uses structural typing to reflect the widespread use of anonymous objects and object literals in JavaScript.

Nominal and structural typing each have advantages and disadvantages.

DistinctionNominal typingStructural typing
Compatibility basisDeclaration names and explicit relationshipsField and method structure
Relationship declarationGenerally requiredCan be omitted when structures match
Domain distinctionStrongTypes with the same structure may be mixed
Reuse of existing objectsExplicit implementation may be requiredCompatible without separate modification
API evolutionDeclaration relationships are explicitStructural changes may have broad effects

Some languages use nominal types together with structural elements. A language may provide nominal class types while supporting structural function types or anonymous records.

Subtyping

Subtyping is a relationship in which a value of one type can be safely used in a location requiring another type.

Dog <: Animal

This means that Dog is a subtype of Animal. If Dog satisfies every condition expected by code requiring an Animal, a Dog value can be passed to that location.

Subtyping does not simply mean that a type has more fields. Method inputs and outputs, mutable fields, exceptions, and state-change rules must also be considered. Cardelli and Wegner classified inclusion polymorphism based on subtypes as one form of universal polymorphism.

Inheritance and subtyping are closely related but are not the same concept.

  • Inheritance is a language structure for reusing or extending implementations and declarations.
  • Subtyping is a compatibility relationship in which a value can be used in a location requiring another type.

Depending on the language, interface subtyping may be provided without inheritance, and a feature may reuse an implementation without creating a safe assignment relationship.

Rust does not use ordinary class-hierarchy subtyping and primarily limits subtyping to lifetime relationships and higher-ranked lifetimes. When lifetimes are erased, most type relationships are treated as type equality.

Covariance, Contravariance, and Invariance

Variance describes how a type parameter inside a type constructor preserves a subtype relationship.

Assuming Dog <: Animal, the key question is the relationship between Container<Dog> and Container<Animal>.

Covariance

A relationship is covariant when the type relationship is preserved in the same direction.

Dog <: Animal
Producer<Dog> <: Producer<Animal>

A producer that creates only Dog values can be used as a producer of Animal values. The caller expects an Animal, and receiving a Dog causes no problem.

Contravariance

A relationship is contravariant when the type relationship is reversed.

Dog <: Animal
Consumer<Animal> <: Consumer<Dog>

A consumer capable of processing every Animal can also process a Dog, so it can be used where a Dog consumer is required.

Invariance

A type is invariant when the subtype relationship is not transferred between generic types.

There is no automatic subtype relationship
between Box<Dog> and Box<Animal>

A mutable container that permits both reading and writing generally must be invariant to remain safe. If Box<Dog> could be treated as Box<Animal>, someone could insert a Cat, violating the assumptions of code using the original Box<Dog>.

Generic interfaces and delegates in .NET can explicitly specify covariance and contravariance, while invariant type arguments require exactly the same type. Return types in producer roles can be covariant, while parameter types in consumer roles can be contravariant.

For function types, parameters are generally contravariant and return values are covariant.

Animal → Dog

This function can accept an Animal and return a Dog. Replacing it with a function that accepts only a narrower input may prevent it from handling an ordinary Animal passed by the caller. Conversely, returning the more specific Dog is safe for a caller expecting an Animal.

Type Conversion and Casting

Type conversion is the process of treating a value of one type as a value of another type or changing its actual representation.

Type conversion can be divided into the following forms.

  • Implicit conversion
  • Explicit conversion
  • Numeric conversion that changes representation
  • Subtype upcasting
  • Downcasting requiring a runtime check
  • Boxing and unboxing
  • Pointer reinterpretation
  • User-defined conversion

A Coercion is a type conversion automatically performed by a language in a particular context. Rust coercions also occur automatically only at defined positions and for limited type relationships.

An upcast generally treats a subtype as a supertype without information loss.

Dog → Animal

A downcast may require a runtime check to determine whether the actual value of a supertype is a particular subtype.

Animal → Dog

The Java specification likewise explains that converting a Thread to an Object can occur without an additional check, while converting an Object to a Thread requires a runtime check that the actual object has the required type.

Numeric conversions may change the range and precision of a value. Extending a small integer into a larger integer type can preserve the value, but converting a real number into an integer or a large integer into a smaller integer may lose information.

A conversion that changes the meaning of a value must be distinguished from reinterpretation, which preserves the bit representation and changes only its interpretation. Arbitrary reinterpretation can violate alignment, valid bit-pattern, and aliasing rules and cause undefined behavior.

Polymorphism

Polymorphism is the property that one function, type, or interface can be applied to several kinds of values. Cardelli and Wegner broadly divided polymorphism into universal and ad hoc polymorphism and divided universal polymorphism into parametric and inclusion polymorphism.

Parametric Polymorphism

Parametric polymorphism abstracts a concrete type into a type parameter so that the same implementation can be applied to multiple types.

identity<T>(value: T) -> T

identity can be applied with the same structure to integers, strings, user-defined objects, and other values. Its implementation uses only operations shared by every type rather than depending on the concrete identity of T.

Parametric polymorphism increases generality and reusability because the implementation does not arbitrarily change according to the type.

Inclusion Polymorphism

Inclusion polymorphism uses a subtype value through a supertype interface.

draw(shape: Shape)

If Circle and Rectangle are subtypes of Shape, both can be passed to the same function. The actual method implementation may be selected according to the runtime type through dynamic dispatch.

Ad Hoc Polymorphism

Ad hoc polymorphism allows one name or operation to have different implementations for different types. Function overloading and operator overloading are representative examples.

add(Integer, Integer)
add(Float, Float)
add(Vector, Vector)

The same name add is used, but the actual implementation may differ according to the types.

Type Classes

A Type class declares operations that a type must provide and systematizes ad hoc polymorphism by defining an implementation separately for each type.

class Eq a where
    (==) :: a -> a -> Bool

A type that provides an instance of Eq can use the equality operation. Wadler and Blott proposed expressing ad hoc polymorphism, such as operator overloading, through type classes.

A Rust trait also defines an abstract interface that a type can implement and may contain functions, associated types, and associated constants. A generic function can use trait bounds to restrict the behavior required of permitted types.

Generics

Generics are a language feature that defines part of a type or function through type parameters so that it can be reused with multiple types.

struct Pair<T> {
    first: T,
    second: T,
}

Constraints can be specified for generic type parameters.

fn maximum<T: Ord>(values: &[T]) -> &T

This function does not permit every type without restriction, but accepts only types that provide ordering comparison.

Generic constraints may take forms such as the following depending on the language.

  • Class or interface upper bounds
  • Trait bounds
  • Type-class constraints
  • Method sets
  • Lifetime constraints
  • Constructor-existence requirements
  • Value-type or reference-type requirements
  • Type sets and operator constraints

Although generics may look similar at the source level, the method used to implement them as executable code may differ.

Type Erasure

Type erasure transforms some or all generic type arguments into a common representation during compilation. Java generics use erasure to preserve backward compatibility, and not every type argument can be directly inspected during execution. The Java specification defines erasure rules for generic types and type variables.

Type erasure allows multiple type instances to share the same executable code, but may limit operations requiring type-parameter information at runtime.

Monomorphization

Monomorphization generates specialized code for each concrete type with which generic code is used. Rust inspects generic use sites at compile time and generates code appropriate for the concrete types.

Monomorphization is advantageous for type-specific optimization and static dispatch, but when many type combinations are used, generated code size and compilation time can increase.

Runtime Type Preservation

Some runtimes preserve generic type arguments during execution. .NET generics retain runtime type information and do not use the same type-erasure model as Java.

This approach can provide more information for reflection and runtime type checking, but requires support from the runtime and metadata.

An actual implementation may not belong completely to only one of these three approaches. It may share code for reference types and specialize value types, or create common code while passing some operations through type-specific helper functions.

Nullability and Optional Types

null is used to indicate that a value is absent or that a reference does not point to an object. When null is implicitly included in every reference type, it is difficult to determine the safety of the following code through types alone.

user.name

A separate check is required to determine whether user is an actual object or null.

A type system supporting null safety generally distinguishes types as follows.

User
User?

User permits only non-null values, while User? permits either a User or null. Kotlin distinguishes nullability within the type system and restricts direct access to nullable values.

C# nullable reference types are implemented through compile-time annotations and flow analysis applied to existing reference types. string and string? are not different runtime class types, but are used by the compiler to warn about potential null-reference use.

Functional languages and Rust primarily use sum types to represent the presence or absence of a value.

enum Option<T> {
    None,
    Some(T),
}

In this structure, absence is an explicit variant, so both cases must be handled through pattern matching or combinator functions.

Even when nullability is expressed through types, null errors do not disappear completely because of interoperability with external code, forced assertions, reflection, and initialization-order issues. The boundary at which type-system guarantees apply and the features that bypass those guarantees must be distinguished.

Flow-Sensitive Types and Type Narrowing

Flow-sensitive type analysis adjusts the type information of the same variable more precisely according to program control flow.

function length(value: string | string[]) {
    if (typeof value === "string") {
        return value.length;
    }

    return value.length;
}

Inside the conditional branch, value is narrowed to a string according to the typeof value === "string" check. In the other branch, it may be narrowed to an array of strings.

TypeScript uses typeof, instanceof, property checks, user-defined type predicates, and control flow to narrow union types.

A similar system is sometimes called Occurrence typing. Typed Scheme introduced an occurrence-typing system that refines the type of a variable according to conditionals and type tests.

Flow-sensitive analysis must also consider whether a variable can change after a check. Shared mutable variables, asynchronous callbacks, and state that can be changed by another thread may invalidate a previous type check. Therefore, a compiler may preserve the narrowed type only for immutable local variables or within analyzable regions.

Gradual Typing

Gradual typing is an approach that allows statically typed and dynamically checked portions to coexist within one program.

Rather than converting an existing dynamically typed codebase into static typing all at once, type information can be added at the level of modules, functions, or expressions. Parts with concrete type information are checked statically, while runtime checks can be inserted when values cross a boundary where their type is unknown.

Gradual typing has a stricter meaning than simply providing static and dynamic checks side by side. It must systematically define the relationship between precise types and unknown types, runtime checks at boundaries, and responsibility for type errors. Research on gradual typing developed toward allowing programmers to control the degree of static checking applied to each part of a program.

TypeScript is a representative practical example of adding static type analysis to JavaScript code, but not every TypeScript feature can be regarded as completely identical to one particular formal model of gradual typing. Features such as any, which broadly bypasses type checking, structural typing, and erased type annotations coexist within the language.

The advantage of gradual typing is that the scope of type application can be expanded while preserving existing code and external libraries. However, checking costs and complexity in tracing errors may arise at the boundary between static and dynamic regions, and boundary rules are needed to prevent untyped values from violating assumptions in static regions.

Refinement Types

A Refinement type adds logical conditions to an existing type to express a more precise range of values.

PositiveInteger = {
    value: Integer | value > 0
}

The ordinary integer type permits every integer, while PositiveInteger permits only integers greater than zero.

Conditions can also be included in function types.

divide:
    x: Integer
    y: Integer where y != 0
    → Integer

This type expresses the precondition that the divisor is not zero.

A refinement-type checker can send logical conditions generated by the program to an automated prover such as an SMT solver. Liquid Type-family systems use a restricted refinement framework that adds logical predicates to types while allowing checking to remain automated. Refinement types can verify more precise program properties than ordinary types.

Examples that can be expressed with refinement types include the following.

  • An array index is within bounds.
  • An integer lies within a particular range.
  • The length of a result array is equal to the input length.
  • A denominator is not zero.
  • A collection is sorted.
  • Access permissions and security levels
  • Arithmetic overflow does not occur.

As logical conditions become more complex, type-checking costs and the difficulty of explaining errors may increase. Systems may restrict the logical domain that an automated prover can decide or require the user to provide supporting proofs where necessary.

Dependent Types

A Dependent type is a type that can depend on a value. Whereas an ordinary generic type accepts another type as a parameter, a dependent type can use values such as natural numbers, Boolean values, and strings to determine a type.

A representative example is a vector whose length is included in its type.

Vector<Element, 3>
Vector<Element, 4>

A vector of length 3 and a vector of length 4 are different types. A vector-concatenation function can express the relationship between lengths through its type.

append:
    Vector<T, n>
    → Vector<T, m>
    → Vector<T, n + m>

The Agda documentation describes dependent types as families of types indexed by objects of another type and gives Vec n as an example of a vector type indexed by the natural number n.

In a dependent function type, the return type may vary according to the input value.

isSingleton:
    Bool → Type

At the type level, it can compute an integer type when the input is true and a list-of-integers type when the input is false. Idris treats types as first-class entities that can be passed to functions, returned, or computed.

Dependent types can include properties such as the following within types.

  • Sizes of arrays and matrices
  • Current protocol state
  • Proof that a syntax tree satisfies typing rules
  • Length and structure of an operation result
  • Proof that a program satisfies a particular theorem
  • Evidence that an index access is valid

Under the Curry–Howard correspondence, a type can be interpreted as a proposition and a value of that type as a proof of the proposition. A function implementation becomes a term proving the proposition expressed by its type. This structure allows programs and proofs to be written in the same language.

Dependent types provide highly precise contracts, but type checking requires value-level computation and normalization. When automatic inference is difficult, types and proof terms must be written directly, and implementation changes may require corresponding proof modifications.

Linear and Affine Types

In an ordinary type system, a value can be used multiple times or not at all. A Linear type restricts a particular value to being used exactly once. An Affine type permits a value to be used at most once and allows it to be discarded without use.

LinearResource

If this resource cannot be copied or consumed twice, resources that must have a single owner—such as file handles, memory buffers, communication channels, and GPU command buffers—can be managed through types.

Linear types restrict how many times a value may be used, controlling duplicated use and omission of resources. Affine types ensure that some values are not used more than once.

Examples of their use include the following.

  • Releasing memory exactly once
  • Closing a file after use
  • Preventing reuse of a lock after it has been released
  • Sending a message only once
  • Managing non-copyable hardware resources
  • Restricting duplication of quantum states
  • Limiting aliasing in concurrent programs

Rust is not identical to a traditional linear type language, but combines ownership, moves, borrowing, and lifetimes with type checking. Ownership rules allow the compiler to check memory-management relationships and form the basis for providing memory safety without a garbage collector.

Rust's reference rules permit either one mutable reference or multiple immutable references at a particular time and prevent a reference from outliving the object to which it refers.

Ownership and linearity demonstrate that a type system can track not only the shape of a value, but also its usage rights and lifetime.

Effect Types

An ordinary function type represents only inputs and outputs.

String → Integer

However, functions with the same input and output types may behave very differently.

  • Purely parsing a string
  • Reading a file
  • Sending a network request
  • Modifying global state
  • Raising an exception
  • Starting an asynchronous operation

An Effect system tracks the side effects that a function may perform together with its type.

readConfig:
    Path → Config
    effects { FileRead, IOError }

An effect system can represent the following.

  • Input/output
  • Reading and modifying state
  • Exceptions
  • Asynchronous execution
  • Continuations and control effects
  • Access to memory regions
  • Creation of concurrent tasks
  • Security permissions
  • Function purity

Lucassen and Gifford proposed an effect system that tracks effects an expression may perform polymorphically and analyzes constraints on parallel execution.

An effect type includes not only the type of a program's execution result, but also properties of the execution process in its interface. It can prevent a state-modifying function from being passed to a location requiring a pure function or require a caller to handle particular exceptions.

Java checked exceptions can be regarded as a limited form of effect tracking, but a general effect system can combine and infer multiple kinds of side effects beyond exceptions.

Session Types

A Session type expresses the order and direction of messages exchanged through a communication channel, along with branching and termination rules, as a type.

Client:
    Send<Request>
    Receive<Response>
    Close

This type represents a protocol in which the client must send a request, receive a response, and then close the channel.

The server-side type expresses communication in the opposite direction.

Server:
    Receive<Request>
    Send<Response>
    Close

Session types can be used to prevent errors such as the following.

  • Sending messages in the wrong order
  • Receiving an unexpected message type
  • Mismatched communication branches
  • A channel terminated by only one side
  • Omitting a stage of a protocol
  • Role mismatches between communication partners

Multiparty session types define an overall communication protocol among more than two participants and can project it into a local type for each participant. Session-type research has developed as a type-based method for structuring communication-oriented programs.

Session types are often combined with linear types. If one channel state can be used arbitrarily in multiple locations, the protocol's progression state may diverge. Restricting channel ownership and usage count can ensure that the next stage proceeds uniquely.

Runtime Representation of Types

Whether types exist only in source code and during compilation or are preserved during execution depends on the language and implementation.

A Reified type retains type information during execution so that it can be used for reflection, type checking, serialization, and similar operations. Class objects, runtime type descriptors, and virtual-method tables may contain this information.

An Erased type has some type information removed from executable code after type checking. Generic type variables, type aliases, and static-only nullability information may be erased.

Type erasure does not mean that types have no effect on execution results. Type information may already have been used to determine the following.

  • Which function or overload to call
  • Memory layout
  • Machine code to generate
  • Runtime checks to insert
  • Generic specialization
  • Destruction and copying behavior
  • ABI and calling conventions
  • Optimization opportunities

Some type information, such as C# nullable reference types, has the same runtime representation but is used only for compiler flow analysis.

Strong and Weak Typing

“Strong typing” and “weak typing” are widely used expressions, but they do not have one agreed formal criterion. Depending on the context, they may refer to different characteristics such as the following.

  • Range of implicit type conversions
  • Possibility of mixing different types
  • Possibility of memory reinterpretation
  • Whether runtime type checking occurs
  • Features capable of bypassing type rules
  • Automatic conversion between strings and numbers
  • Pointer operations and undefined behavior
  • When type errors occur

For example, it is difficult to place a statically typed language with many implicit numeric conversions and a dynamically typed language with few automatic conversions but runtime checking on a simple strong-to-weak scale.

When an accurate explanation is needed, it is better to describe specific properties directly.

  • It uses static type checking.
  • It permits implicit conversion between strings and numbers.
  • Downcasting requires a runtime check.
  • A pointer can be reinterpreted as an arbitrary type.
  • Generic type arguments are preserved during execution.
  • A type error raises an exception.
  • Safe code does not permit memory reinterpretation.

The Java specification describes Java as a statically and strongly typed language, but this is in the context of explaining the type and operation restrictions defined by Java. It does not establish a universal strong-to-weak scale covering other languages.

Type-System Bypasses and Boundaries

Practical languages provide bypass features for situations that are difficult to express through the type system or require interoperability with external systems.

Representative examples include the following.

  • Explicit casts
  • Dynamic types
  • Check-exempt types such as any
  • Raw pointers
  • unsafe blocks
  • Reflection
  • Foreign function interfaces
  • Serialization and deserialization
  • Native plugins
  • Type assertions
  • Compiler intrinsics

These features may be necessary for systems programming, performance optimization, and connecting to low-level APIs. However, the programmer or external code becomes directly responsible for conditions that the type checker previously proved.

To create a safe boundary, bypass features should be limited to a small internal implementation while exposing an interface that satisfies the type rules externally. Wrapping an unsafe implementation inside a safe Rust function is an example of this structure.

External data also exists outside the trust boundary of the type system. JSON, network packets, files, and database rows do not automatically satisfy the types expected by a program. Their structure and value ranges must be validated during deserialization before they are converted into internal types.

Type-System Design Choices

A type system is not designed solely by maximizing expressiveness and safety without limit. Language designers choose a balance among the following elements.

  • Accuracy of type checking
  • Amount of type annotation
  • Possibility of type inference
  • Compilation time
  • Comprehensibility of error messages
  • Compatibility with existing code
  • Dynamic features and reflection
  • Execution performance and code size
  • Separate compilation and module boundaries
  • Interoperability with external languages
  • Analyzability by tools and IDEs
  • Complexity of the language specification and implementation

A more precise type system can discover more errors before execution and document program properties. However, type-checking problems may become more complex, and the amount of type annotation and proof code may increase. Conversely, a simpler type system makes the language and implementation easier to understand, but may leave more program invariants to runtime checks and tests.

Modern languages combine several levels of checking rather than choosing only one method. They may provide fundamental static typing and type inference and add sum types and pattern matching, generic constraints, nullability, ownership, refinement types, or external static analyzers where needed.

The purpose of a type system is not to accept every possible program. Rejecting programs that the language cannot determine to be safe is also part of its role. Even when a program is actually safe, the type checker may reject it if there is insufficient information to prove that fact. In such cases, the program structure can be changed, type annotations and proofs can be added, checking can be deferred until runtime, or a limited unsafe boundary can be used.

Therefore, a type system is not merely a mechanism for attaching labels to values, but a formal system that expresses program structure, the range of valid computation, abstraction and resource ownership, errors and side effects, and communication protocols. Beginning with fundamental features such as static and dynamic checking, type inference, subtyping, polymorphism, generics, and algebraic data types, it can extend to dependent types, linear types, effect types, and session types, determining which programs a programming language can express and which errors it can prevent in advance.

Language Design and Standardization

Designing a programming language is not limited to creating new keywords and syntax. It is the task of determining the problems the language is intended to solve and its primary usage environments, then organizing values and types, state changes, function calls, memory management, error handling, concurrency, modules, execution methods, and interoperability rules with external systems into one consistent computational system. A programming-language ecosystem capable of long-term use is formed only when this system is combined with a language specification, implementations, a standard library, development tools, change procedures, and compatibility policies.

The standardization of a programming language is the process of defining the language's syntax and semantics in a common document and establishing criteria so that different implementations process the same program as consistently as possible. Standardization aims to improve the portability of programs and libraries and to establish a language ecosystem that is not dependent on one particular implementation. SC 22, a subcommittee of ISO/IEC JTC 1, is the international standardization body responsible for programming languages, their execution environments, and system-software interfaces, with portability as one of its principal objectives.

Not every programming language has an international standard. C (programming language) and C++ are standardized through ISO/IEC working groups, while ECMAScript, the foundational specification of JavaScript, is developed by TC39 of Ecma International. Changes to Python are discussed through PEPs, changes to Rust through RFCs, changes to Go (programming language) through its proposal process, changes to Kotlin through KEEP, and changes to Swift through Swift Evolution. The development of a programming language may therefore be led in different ways by international standards organizations, industry consortia, companies, open-source projects, and independent communities.

Design Goals and Application Domains

When designing a new programming language, the problems the language is intended to solve and its primary users must first be identified. It is difficult to achieve the highest execution performance, the simplest syntax, the strongest safety, the fastest compilation, the greatest portability, and complete backward compatibility across every field at the same time. Language design is a process of selecting priorities among goals that may conflict with one another.

Major design goals may include the following.

  • Whether hardware and system resources can be controlled in detail
  • To what extent memory errors and type errors are prevented
  • Whether program behavior and execution costs can be predicted
  • Whether code can be expressed concisely and clearly
  • Whether compilation and program startup are fast
  • Whether long-running programs achieve high performance
  • Whether programs are easy to port across operating systems and processors
  • Whether the language supports large codebases and collaboration among multiple developers
  • Whether existing languages, libraries, and operating-system APIs can be reused
  • Whether concurrency and parallelism can be expressed safely
  • Whether compilers, runtimes, and development tools are easy to implement
  • Whether source-code and library compatibility can be maintained over long periods
  • Whether error messages and the debugging experience are easy to understand

Systems programming languages often place importance on memory layout and lifetimes, calling conventions, foreign function interfaces, and the predictability of latency and execution costs. Languages used primarily for automation or data analysis may prioritize interactive execution, concise expression, dynamic data structures, and access to libraries. Educational languages emphasize conceptual clarity, consistent syntax, and understandable error messages, while domain-specific languages focus on expressing concepts from a particular field briefly and precisely.

The target execution environment of a language also directly affects its design. When targeting operating systems and embedded systems, standalone executables, small runtimes, clear ABIs, and operation under limited resources become important. For a language running in a web browser, sandboxing, incremental code loading, and connection to the host environment are important. In a language for distributed services, lightweight tasks, failure isolation, network communication, observability, and deployment tools may become central elements.

Design Principles

Language designers should establish fundamental principles that apply across the entire language rather than adding individual features independently. When each feature uses different rules, the language becomes difficult to learn and implement, and combining features may produce behavior that is difficult to predict.

Representative design principles include the following.

  • Consistency gives similar concepts similar syntax and semantics.
  • Orthogonality allows a small number of independent features to be combined in multiple ways.
  • Explicitness makes important state changes, error possibilities, and costs visible in source code.
  • Abstraction hides implementation details while preserving necessary semantics and performance characteristics.
  • Local reasoning reduces situations in which the entire program must be examined to understand one portion of the code.
  • Conservative extension prevents new features from unnecessarily changing the meaning of existing programs.
  • Implementability ensures that language features can be implemented with realistic compilers and runtimes.
  • Tool friendliness allows parsers, formatters, debuggers, static analyzers, and IDEs to analyze code reliably.
  • Interoperability allows the language to connect to operating systems, external libraries, other languages, and existing data formats.
  • Simplicity keeps the number of concepts and exceptional rules in the language as small as possible.

High orthogonality does not mean that every feature should be combinable without restriction. Even when individual features are simple, a combination of two features may produce ambiguous semantics or an excessively complex implementation. Generics, overloading, type inference, implicit conversion, and subtyping are each useful, but providing them together greatly increases the complexity of type checking and error diagnostics. Language design must therefore examine not only the number of features but also the interactions among them.

The simplicity of a language likewise does not merely mean having few keywords. Even with concise syntax, numerous hidden implicit behaviors and exceptional rules can make the actual semantic system complex. Conversely, even when a language has somewhat more syntactic elements, program behavior may be easier to predict when the role of each element and the rules governing their combination are clear.

Core Computational Model

A language must determine the computational model through which programs are expressed. Imperative languages center on changes in storage state and the execution order of commands, while functional languages center on function evaluation and value transformation. Object-oriented languages center on objects and messages or method calls, while logic languages center on facts, rules, and queries.

Modern programming languages often support multiple paradigms, but not every paradigm carries equal weight within the language. Even when functions are first-class values, a language may be used primarily in an imperative and object-oriented style if its fundamental structure and standard library are centered on mutable objects and imperative control flow.

The following elements must be designed together when determining a computational model.

  • What values and objects are
  • Whether values have identity and mutability
  • How functions and methods are called
  • In what order expressions are evaluated
  • Where program state is stored and how it is changed
  • How side effects and input/output are represented
  • How errors and failures are propagated
  • How concurrent tasks share or exchange state
  • Into what units programs are decomposed and how those units are combined

It is important to define the computational model semantically before determining the syntax. The same computational model can be expressed through different syntax, but deciding syntax before the meaning is clear can cause exceptional rules and implicit behavior to accumulate when later features are added.

Values and the Data Model

Language design must define what values are handled by programs. The data model of a language may include not only primitive values such as integers, real numbers, Boolean values, characters, and strings, but also arrays, tuples, records, functions, objects, pointers, references, sum types, and modules.

A data model must answer questions such as the following.

  • Whether values are copied directly or shared through references
  • Whether values have unique identity
  • Whether a value can be modified after it is created
  • How identity and equality are distinguished
  • What representation a value has in memory
  • Who manages the lifetime of a value
  • Whether cyclic references are permitted
  • Whether functions and types can also be treated as values
  • Whether type information is preserved at runtime

The distinction between value types and reference types is not defined simply as a difference between the stack and the heap. The central issue is whether assignment and argument passing copy a value or share a reference and whether a change can be observed through another reference. In an optimized implementation, a value type may be placed on the heap or an object of a reference type may be placed on the stack, so language semantics must be distinguished from concrete memory placement.

State and Mutability

Whether program state can be changed and within what scope such changes are permitted are also central design issues. Mutable variables and objects make it easy to represent the state of real systems directly, but when the same state is shared across multiple parts of a program, the program's behavior can become difficult to reason about.

A language can control mutability in the following ways.

  • Make every variable mutable by default.
  • Distinguish immutable and mutable variables through separate declarations.
  • Distinguish the mutability of an object from the mutability of a reference.
  • Express through types whether a function modifies external state.
  • Restrict modification of a value to a single owner.
  • Permit shared values to be accessed only immutably.
  • Restrict state changes to the internals of a particular module or object.

Immutability does not mean eliminating program state entirely. It is a method of controlling the location and scope of changes by creating new values or passing state explicitly through arguments and return values. Conversely, even languages that provide mutability can localize state changes through encapsulation, ownership, and access restrictions.

Type-System Design

A type system defines the relationships among the values and operations permitted by a language. Language designers must determine how to combine static and dynamic type checking, type inference, subtyping, generics, overloading, type conversion, and runtime type information.

Type-system design includes choices such as the following.

  • Whether types are checked before execution or during execution
  • To what extent programmers must write types explicitly
  • Whether type names or structures are used as the basis of compatibility
  • To what extent implicit type conversion is permitted
  • How generic code is expressed and implemented
  • Whether nullability and error states are included in types
  • Whether the type checker tracks value ownership and lifetimes
  • Whether function side effects and exceptions are represented in types
  • At what boundaries unsafe operations are permitted

Increasing the expressive power of a type system allows more errors and invariants to be checked at compile time, but type checking and error messages may become more complex. Conversely, a simple type system makes the language and implementation easier to understand, but leaves more conditions to runtime checks and testing.

Type inference should not be designed solely to omit every possible annotation. Inferring obvious types for local variables while requiring explicit types at public functions and module boundaries can reduce verbosity while preserving clear API contracts.

Control Flow and Evaluation Rules

Control flow determines which parts of a program execute and in what order. A language must define rules for sequential execution, conditional branching, iteration, function calls, recursion, exception handling, pattern matching, coroutines, and asynchronous execution.

When evaluation order is unclear, implementations may produce different results for expressions with side effects. A language specification must therefore determine matters such as the following.

  • In what order function arguments are evaluated
  • Whether logical operators use short-circuit evaluation
  • The precedence and associativity of assignment and operators
  • When temporary values and local variables are created and destroyed
  • What cleanup operations are performed when an exception occurs
  • At what point an asynchronous function suspends and resumes execution
  • In what order memory operations are observed across multiple threads

When a language leaves some evaluation order unspecified, implementations can select among multiple orders for optimization. However, if a programmer depends on an unspecified order, results may differ by implementation or optimization level. When such freedom is permitted, the specification must clearly distinguish what is guaranteed from what is not guaranteed.

Functions and the Calling Model

Functions are fundamental units for separating and reusing computation in most programming languages. Function design includes not only declaration syntax, but also parameter passing, returns, capture, overloading, polymorphism, and calling rules.

A language must determine matters such as the following.

  • Whether arguments are passed by value or by reference
  • Whether call-by-name or lazy evaluation is supported
  • Whether default and variadic arguments are permitted
  • Whether functions can be treated as first-class values
  • How closures capture external variables
  • Whether function and operator overloading are permitted
  • Whether method calls use dynamic dispatch
  • Whether tail-call optimization is guaranteed
  • How errors and effects that a function may return are represented
  • Which ABI is used when calling external code

Whether a closure copies an external variable by value or captures it by reference and how the lifetime of the captured variable is extended are closely connected to the language's memory model. The function-calling model must also be designed together with the type system, object model, and runtime implementation.

Memory and Resource Management

Language designers must determine how to manage the lifetimes of objects, memory, files, network connections, locks, GPU resources, and similar resources.

Representative approaches include the following.

  • The programmer directly allocates and releases resources.
  • Objects are released automatically by counting references.
  • A garbage collector traces reachable objects.
  • Objects are managed by regions or arenas.
  • Ownership and lifetimes are checked statically.
  • Destructors or cleanup functions are executed automatically when a scope ends.
  • Multiple approaches are used together.

Automatic memory management does not mean that external resources are also cleaned up automatically at the appropriate time. Even in languages with garbage collectors, resources that must be released immediately, such as files, sockets, and locks, require separate cleanup structures.

The memory-management model affects execution performance, latency, safety, ABI design, interoperability with external languages, and program structure. Manual memory management allows execution costs to be controlled precisely, but programmers must manage risks such as use after free, double free, and leaks. Garbage collection automates object lifetimes but may introduce collection costs, pauses, and runtime complexity. Ownership-based approaches can prevent many errors statically, but require additional restrictions on reference relationships and lifetimes.

Error Handling

A language must define how failures are represented and propagated. It may return error codes, raise exceptions, or represent success and failure through sum types or result types.

Error-handling design must consider the following.

  • Whether recoverable errors and programming defects are distinguished
  • Whether errors are expressed in function types
  • Whether callers are required to handle errors
  • How exceptions move across function boundaries
  • How resources are cleaned up during stack unwinding
  • Whether multiple errors can be preserved simultaneously
  • Where errors from asynchronous and concurrent tasks are propagated
  • How errors are converted at boundaries with external languages

Exceptions can make error propagation concise, but it may be difficult to determine from a function declaration which exceptions can occur. Result types make the possibility of failure explicit, but repetitive propagation code may increase. A language can combine syntax, the type system, pattern matching, and propagation operators to reduce these costs.

Concurrency and the Memory Model

Modern languages must account for environments in which multiple tasks execute concurrently. Merely providing an API for creating threads is insufficient; a Memory model is needed to define the order in which multiple execution units observe memory operations.

Concurrency design includes choices such as the following.

  • Whether shared memory and threads are used
  • Whether message passing and channels are central
  • Whether actors and isolated state are provided
  • Whether coroutines and asynchronous functions are provided
  • How atomic operations and memory ordering are defined
  • Whether data races are treated as errors or undefined behavior
  • How task cancellation and failure propagation are structured
  • Whether structured concurrency is included in the language
  • How parallel iteration and data parallelism are expressed

Compilers and CPUs may reorder memory operations for performance. The language's memory model must define which reorderings are permitted and what ordering relationships are formed by synchronization operations. When the memory model is unclear, the same program may behave differently depending on the processor and compiler.

Modules and Namespaces

A module system divides a program into multiple units and distinguishes the public interface of each unit from its internal implementation.

Module design must determine the following.

  • Whether one file and one module are the same unit
  • Whether multiple files can form one module
  • Whether namespaces can be nested
  • How public and private declarations are distinguished
  • How name conflicts are resolved when importing modules
  • Whether cyclic dependencies are permitted
  • Whether modules can be compiled separately
  • In what format information about public types and functions is stored
  • How packages, modules, and libraries are distinguished
  • Whether different versions of a package can be used simultaneously

A good module system hides implementations, makes dependencies clear, and supports incremental compilation. Conversely, if too many implementation details are exposed through public interfaces, even a small internal change may cause recompilation of dependent modules or compatibility problems.

Interoperability and External Interfaces

For a new programming language to be used in real environments, it must be able to connect to existing operating systems, libraries, data formats, and other languages. Language interoperability is especially important when establishing an initial ecosystem.

Interoperability design includes the following elements.

  • Whether C ABI and native functions can be called
  • In what memory formats structures, arrays, and strings are passed
  • Which side owns objects and is responsible for deallocation
  • Whether callbacks can be passed to another language
  • Whether exceptions can cross language boundaries
  • Whether runtime-managed objects can be pinned to prevent movement
  • How generics, classes, and interfaces from other languages are represented
  • Whether binaries and runtimes of different versions can be used together

External interfaces may form boundaries where the language's safety guarantees end. If an external function returns an invalid pointer or length, an error can occur even inside a safe language. A layer is therefore needed to validate external data and transform it into safe internal types.

Syntax Design

Syntax is the means by which a language's concepts are represented through characters and tokens. Syntax design must consider not only code length but also readability, ambiguity, error recovery, and tool processability.

Syntax design must determine matters such as the following.

  • The form of identifiers and keywords
  • How statements and blocks are separated
  • The meaning of whitespace and indentation
  • Operator precedence and associativity
  • The form of function, type, and module declarations
  • Syntax for generics and type annotations
  • Formats for comments and documentation comments
  • Notation for string and numeric literals
  • Syntax for macros and metaprogramming
  • Possibility of recovery after a syntax error

Syntax should avoid ambiguous structures that can be interpreted in multiple ways. Even when a parser can interpret an expression, syntax that humans can easily misunderstand may reduce maintainability. Adding a new keyword can also break existing programs that used the same name as an identifier, so reserved-word policy is connected to the long-term evolution of the language.

Syntax design also affects automatic formatters, IDEs, and code generators. If token meanings vary excessively according to context or grammar depends on information from other files, independent parsing and editor support become difficult.

Separating Syntax and Semantics

A language specification distinguishes syntactically valid programs from semantically valid programs. Grammar defines how tokens may be combined into structures, while the declaration of names, type compatibility, access permissions, and evaluation results are determined by separate semantic rules.

For example, the following expression may be syntactically valid.

left + right

However, whether addition can be applied to left and right, which implementation of addition is selected, and what the result type is are determined by semantic analysis and the type system.

A language specification generally explains the following layers separately.

  • Lexical grammar
  • Syntactic grammar
  • Static semantic rules
  • Dynamic execution semantics
  • Library interfaces
  • Areas in which implementations may choose behavior
  • Behavior on which programs must not depend

When grammar is detailed but semantics are unclear, different implementations may process the same source code differently. Conversely, if semantics are defined too closely according to one implementation procedure, new optimization or implementation strategies become difficult to use. A specification must define results and observable behavior with sufficient clarity while avoiding unnecessary restrictions on the internal structure of implementations.

Language Specifications

A Language specification is a normative document defining which source programs are valid and what valid programs mean. It differs from a document that merely describes the current behavior of a particular compiler. Implementations must follow the specification, and behavior that differs from the specification is treated as an implementation defect or an ambiguity in the specification.

A typical language specification includes the following.

  • Character sets and source-code encoding
  • Tokens and lexical grammar
  • Syntactic grammar
  • Names and scopes
  • Types and conversions
  • Expressions and statements
  • Declarations and modules
  • Execution order and evaluation rules
  • Object and memory models
  • Concurrency and atomic operations
  • Errors and exceptions
  • The standard library
  • Implementation-conformance requirements
  • Implementation-defined, unspecified, and undefined behavior
  • Relationships with other standards

The C (programming language) standard defines the form and interpretation of programs expressed in C and aims to provide a common foundation for portable programs and conforming implementations. WG14 is responsible for international standardization of C, while WG21 is responsible for international standardization of C++.

Normative and Informative Content

Standards documents may distinguish normative content, which implementations must follow, from informative content intended to aid understanding.

Normative content may contain requirements such as the following.

  • An implementation must perform a particular action.
  • A program must satisfy a particular condition.
  • A diagnostic must be provided when a condition is violated.
  • A result must fall within a defined range.

Informative content provides examples, design background, recommendations, references, and similar material, but may not directly determine implementation conformance.

When this distinction is unclear, disputes may arise over whether example code and explanatory sentences should be interpreted as rules. Formal specifications should therefore clearly distinguish requirements from reference explanations.

Conforming Programs and Implementations

A language standard may define conformance not only for programs but also for implementations. A conforming implementation must support the syntax, semantics, and library features required by the standard and provide required diagnostics.

An implementation may be allowed to provide extensions beyond the standard. However, it must not change the meaning of conforming programs that do not use those extensions or reject standard programs. Implementations may provide separate extension modes and strict standards modes.

Conformance does not mean that every implementation must generate identical machine code for every program. Optimization, memory layout, and internal data structures may be selected freely, but program-observable results must remain within the range permitted by the specification.

Implementation-Defined, Unspecified, and Undefined Behavior

A language specification does not necessarily fix every detail to a single behavior. To allow differences among hardware and operating systems or preserve freedom for implementation optimization, some behavior may be divided into several categories.

Implementation-Defined Behavior

Implementation-defined behavior allows an implementation to select one of several alternatives, but requires it to document which choice it makes.

Examples of implementation-defined matters may include the following.

  • The size of primitive integer types
  • Whether the plain character type is signed
  • Handling of particular files and character encodings
  • Extension types provided by the implementation
  • Platform-specific calling conventions

A program may rely on such behavior after consulting implementation documentation, but the result may differ when ported to another implementation.

Unspecified Behavior

Unspecified behavior permits several results without requiring the implementation to document which one it chooses. Even within the same implementation, the choice may vary according to optimization or execution conditions.

A program should assume only that one of the permitted results will occur and must not depend on a particular result.

Undefined Behavior

Undefined behavior is behavior for which the specification imposes no requirements. When such behavior occurs, an implementation does not guarantee any particular result.

Undefined behavior is sometimes used in languages close to hardware to permit high performance and diverse implementations. However, if programmers trigger it during normal execution, it can lead to security vulnerabilities and unpredictable failures.

Undefined behavior must be distinguished from implementation defects. If a program follows the specification and an implementation produces an incorrect result, that is an implementation defect. Conversely, if a program performs behavior prohibited by the specification, the absence of a particular result does not by itself make the implementation defective.

Relationship Between Specifications and Implementations

A programming language may have multiple implementations. Each implementation may use different compiler architectures, optimization methods, virtual machines, garbage collectors, standard-library implementations, and target platforms.

The existence of multiple implementations provides advantages such as the following.

  • It reduces dependence on a particular company or project.
  • Different implementation strategies can be used for different execution environments.
  • Ambiguities in the specification can be discovered by comparing implementations.
  • A defect in one implementation can be checked against another.
  • New optimization and runtime structures can be tested.

However, if multiple implementations process the same program differently in practice, the ecosystem may fragment. A clear specification, conformance tests, and differential testing between implementations are needed to prevent this.

In a language with only one implementation in practical use, the behavior of that implementation may serve as an informal specification. However, if accidental implementation behavior is not distinguished from intended language rules, later optimizations and defect fixes may break existing programs.

Reference Implementations

A Reference implementation is a representative compiler or interpreter that implements a language specification. It allows language features to be tested in practice and provides a baseline against which other implementations can compare behavior.

However, a reference implementation is not identical to the language specification. It may contain defects and uses only one among the implementation methods permitted by the specification. Another implementation may differ in internal structure or performance characteristics while still being conforming if it satisfies the specification.

When a reference implementation becomes an excessively strong authority, implementation details not present in the specification may solidify into de facto standards. A language project should therefore clearly distinguish intended rules through specifications and tests.

Conformance Testing

Conformance testing is a collection of tests that verifies whether an implementation satisfies the requirements of the language specification and standard library.

Conformance testing may include the following.

  • Confirming that valid programs are processed normally
  • Confirming that invalid programs receive required diagnostics
  • Confirming execution results for expressions and control flow
  • Confirming type conversions and overload selection
  • Confirming standard-library results and error conditions
  • Confirming memory-model and concurrency behavior
  • Confirming the structure of file and binary formats
  • Comparing results across different platforms

Passing conformance tests does not mean that an implementation contains no defects. The number of possible programs is effectively infinite, so tests verify representative rules and boundary conditions from the specification. Random program generation, fuzzing, differential testing, and formal verification can be used together to discover more implementation defects.

The Java specification explicitly addresses requirements that independent implementations must satisfy and compatibility tests they must pass. The Java Language Specification also defines binary compatibility rules for classes and interfaces in a separate chapter in addition to the source language.

Standard Libraries

The practical usability of a programming language depends greatly not only on its core syntax but also on its standard library. Designers must determine the extent to which functions for strings, collections, files, time, mathematics, concurrency, networking, serialization, and other areas are standardized.

A small standard library is easier to implement and port and allows various external libraries to compete. However, when even basic functionality depends on external packages, different interfaces may be used in each project and the ecosystem may fragment.

The following should be considered when designing a standard library.

  • The boundary between core language features and library features
  • Features that can be provided on every platform
  • How differences between operating systems are abstracted
  • Error-handling methods
  • Concurrency and asynchronous APIs
  • Binary compatibility
  • API naming and consistency
  • Procedures for deprecating old features
  • Division of roles with the external package ecosystem

Standard-library APIs may directly affect a broader range of user code than language syntax. Since names and behavior of publicly released types and functions are difficult to change, compatibility policy and extensibility must be considered from the beginning.

Language Change Proposals

Mature languages generally use an established proposal process instead of adding features immediately. A proposal document should explain not only the problem and solution, but also its relationship with existing features, alternatives, compatibility, implementability, and disadvantages.

A typical proposal document includes the following.

  • The problem to be solved
  • Actual use cases
  • Proposed syntax and semantics
  • Type-checking and execution rules
  • Compatibility with existing code
  • Effects on the standard library and tools
  • Implementation methods and performance costs
  • Security and safety effects
  • Alternatives considered but not adopted
  • Unresolved questions
  • Possibilities for future extension
  • Testing and documentation plans

A Python PEP is a design document that provides information to the Python community or describes new features for the language, procedures, or environment, and it is required to provide both a technical specification and the rationale for the proposal.

The Rust RFC process was established so that major changes to the language and standard library pass through a consistent and controlled path. Proposals undergo public discussion and team review before being accepted or rejected, and an accepted RFC does not immediately become a stable feature. Separate implementation, testing, and stabilization processes are required.

In the Go proposal process, an issue briefly explaining the problem is first submitted. The review process may accept or reject it immediately or require a more detailed design document. Proposal review generally seeks consensus, and proposals that conflict with the language's core design or are difficult to implement efficiently may be rejected.

Kotlin's KEEP repository manages both ongoing proposals and design documents for already implemented changes to the language and standard library.

Swift Evolution reviews user-visible design changes to the Swift language, standard library, and core tools through an open process. It aims to reflect a broad range of community opinions while maintaining the overall direction and conceptual consistency of the language.

Gradual Development of Proposals

A language feature generally does not enter the formal standard immediately from a single proposal. It may pass through stages such as the following.

Idea and problem identification
    ↓
Initial design and use cases
    ↓
Public proposal document
    ↓
Discussion and revision
    ↓
Prototype implementation
    ↓
Testing and collection of real-world experience
    ↓
Specification wording
    ↓
Stabilization review
    ↓
Inclusion in the formal language or standard

At the initial stage, reviewers determine whether the problem being addressed is sufficiently important in practice. Even a seemingly useful feature may not be added to the language itself if it can already be solved adequately through existing syntax and libraries or if its use cases are limited.

A prototype is used to confirm that the proposal is actually implementable and to discover problems involving syntax, type checking, error messages, performance, and tool support. When a feature is determined solely through documentation without implementation experience, hidden complexity may be discovered only later.

The ECMAScript TC39 process uses multiple stages so that proposals advance from an initial idea through investigation and design, implementation feedback, and completion of specification wording before inclusion in the formal specification. Proposals reaching the completed stage must satisfy conditions such as actual implementations, tests, and editable specification text.

Experimental Features and Stabilization

When a new feature is provided immediately as stable, it becomes difficult to correct design defects discovered later. Languages and platforms may therefore use experimental features, preview features, and incubator APIs.

The following may be permitted during an experimental stage.

  • Syntax and semantics may change in later versions.
  • An explicit compiler option may be required.
  • The feature may be provided in a separate standard-library namespace.
  • Use in production environments may not be recommended.
  • Compatibility guarantees may be limited.
  • Usage experience and implementation feedback may be collected.

The following should be reviewed before stabilization.

  • Whether sufficient real-world use cases exist
  • Whether syntax and semantics are consistent with existing features
  • Whether implementations can support the feature efficiently
  • Whether error messages and tool support are sufficient
  • Whether the design leaves room for future extension
  • Whether there are safety or security concerns
  • Whether it changes the meaning of existing programs
  • Whether other implementations can implement it independently

OpenJDK uses the JEP process to track major features and changes to the JDK and the Java platform. Incubator modules and preview features are also managed through the JEP process.

When an experimental feature remains experimental for too long, the boundary between stable and experimental functionality may become unclear. Conversely, stabilizing a feature without sufficient user experience may force an incorrect API or syntax to be maintained for a long period. The purpose and completion criteria of the experimental period should therefore be determined in advance.

Consensus and Decision-Making

Language changes are often difficult to decide through a simple majority vote. A single feature may affect existing users, implementation developers, tool developers, and library authors in different ways.

A typical consensus process considers the following factors.

  • Technical validity
  • The long-term direction of the language
  • Actual user needs
  • Burden on implementations and tools
  • Compatibility with existing code
  • Availability of alternatives
  • Community opinion
  • Maintenance costs
  • The responsible team's ability to provide continuing support

Consensus does not mean that every participant must prefer the proposal. It may mean that significant objections have been sufficiently examined and that the responsible decision-making body has enough information to evaluate the proposal's benefits and costs.

Open discussion helps discover different use cases and problems, but the number of participants in a discussion may not represent the entire user population. Language designers should therefore use not only discussion comments but also code studies, performance measurements, user surveys, existing error cases, and prototype experience.

Governance

Governance is the system that defines who makes decisions about a language's technical direction and changes and through what procedures.

Programming-language governance can take forms such as the following.

  • One designer or a small number of designers make final decisions.
  • An internal corporate team manages the language and implementation.
  • A core team and subteams of an open-source project divide responsibilities.
  • A committee composed of industry members decides by consensus.
  • An international standardization process involving national representatives is used.
  • A foundation or nonprofit organization provides administration and resources.

The governance structure affects the speed, transparency, consistency, and accessibility of decision-making. Leadership by one designer may maintain a consistent direction and enable rapid decisions, but can create strong dependence on that individual. A committee model can represent diverse interests but may require considerable time to make decisions.

A good governance system should clearly publish the following.

  • How proposals are submitted
  • The people and organizations responsible for reviewing proposals
  • Criteria for acceptance and rejection
  • How opinions can be presented
  • How conflicts of interest are handled
  • The extent to which decisions and meeting records are published
  • Procedures for reconsidering previous decisions
  • The relationship of authority between project teams and standardization committees

Even when the technical specification of a language is public, external implementers and users may find it difficult to predict the long-term direction if the actual decision-making process is opaque.

International Standardization

International standardization is a process in which national standards bodies and experts participate in creating common specifications. In programming languages, SC 22 under JTC 1, the joint technical committee of ISO and IEC, plays a major role. The scope of SC 22 includes programming languages, execution environments, and system-software interfaces.

International standardization of C (programming language) is handled by WG14. WG14 is the working group responsible for the C language standard and related technical specifications.

International standardization of C++ is handled by WG21. WG21 develops the C++ standard and technical documents, including core language rules and the standard library. Proposals are submitted as committee documents and may be incorporated into the draft standard after study, evolution, and wording review by specialized groups.

Rather than designating the source code of one particular implementation as the standard, international standards define through documents the requirements that programs and implementations must satisfy. Standards documents may be revised periodically, and separate procedures may handle defect reports and interpretation questions.

An advantage of international standardization is that it provides a common basis independent of a particular company or implementation. However, because national procedures, consensus, and document review are required, changes may progress more slowly than in an open-source project.

Industry Standards and Consortia

Some languages develop primarily through industry organizations or consortia rather than international standards.

ECMAScript is a language specification managed by TC39 of Ecma International. TC39 manages proposal documents and specification wording in public repositories and discusses the progress of proposals at regular meetings. Since compatibility among actual JavaScript implementations and the web ecosystem is important, experience from multiple browser and runtime implementers is reflected in the proposal process.

This approach can develop features more quickly than international standardization and makes it easier to incorporate feedback from actual implementations directly into the specification. However, participating companies and major implementers may have relatively greater influence, so public procedures, meeting records, and consensus criteria are important.

Development Processes of Open Projects

A language managed by an open-source project may develop through code repositories, issue trackers, and public proposal documents.

The Python PEP system can address not only language features but also development procedures, governance, and packaging standards. Since PEP numbers, states, and revision histories are preserved, the background of rejected or withdrawn designs can be examined in addition to accepted features.

The Rust RFC system requires major language and standard-library changes to pass through public design documents. Responsible organizations such as the language team and library team review proposals, while implementation and stabilization continue through separate procedures even after an RFC is accepted.

The Go proposal process does not require every small change to have a long design document. Initial issue review determines the significance and complexity of the proposal, and a detailed design document is requested only when necessary. This adjusts the cost of the proposal process to the scale of the change.

Kotlin's KEEP and Swift Evolution likewise develop their languages and standard libraries through proposal documents, public discussion, and decisions by responsible teams.

Compatibility

Once a programming language becomes widely used, avoiding the breakage of existing programs may become more important than adding new features. Compatibility is not one single concept but consists of multiple layers.

TypeMeaning
Source compatibilityWhether existing source code can be recompiled by a new compiler
Binary compatibilityWhether libraries and programs that have not been recompiled can operate together
Behavioral compatibilityWhether programs preserve their previous meaning and results
API compatibilityWhether the usage of public functions, types, and modules is preserved
ABI compatibilityWhether calling conventions, symbols, and object layouts are preserved
Data compatibilityWhether existing file, network, and serialization formats remain readable
Tool compatibilityWhether build tools, debuggers, formatters, and analyzers continue to work
Ecosystem compatibilityWhether packages and frameworks can be used together under the new language version

Source compatibility may be preserved while binary compatibility is broken. Existing code may work when rebuilt with a new compiler but may not be directly linkable with a library produced by an older compiler.

Conversely, binary compatibility may be preserved while behavior changes subtly. If the error handling, sorting order, or floating-point results of a standard-library function change, the program may still execute but produce different results.

The Java Language Specification specifically distinguishes changes to classes and interfaces that preserve compatibility with existing binaries from those that do not.

Backward Compatibility

Backward compatibility is the property that a new version of a language or implementation can continue to process programs written for an earlier version.

Maintaining backward compatibility requires attention to the following.

  • Whether new keywords conflict with existing identifiers
  • Whether overload and type-inference results change
  • Whether existing grammar becomes ambiguous with new grammar
  • Whether the addition of a standard-library function creates name conflicts
  • Whether code previously treated as an error is interpreted as valid code with a different meaning
  • Whether execution order and numeric results change
  • Whether existing ABIs and serialization formats are preserved

Absolute compatibility is difficult to guarantee. Existing behavior may need to change to correct a security vulnerability or an obvious specification defect. Programs may also break under a new implementation if they depended on behavior not guaranteed by the specification.

The Go 1 compatibility document states an expectation that existing Go 1 programs will continue to compile and run under later Go 1 implementations, while explaining that exceptions may exist for security issues and unspecified behavior.

Deprecation and Removal

Even when a better feature becomes available or an unsafe feature is no longer recommended, immediately removing an existing feature may break many programs. A Deprecation process is therefore used.

A typical deprecation process is as follows.

Provide a replacement feature
    ↓
Mark the existing feature as scheduled for deprecation
    ↓
Issue warnings from the compiler and documentation
    ↓
Provide automatic migration tools
    ↓
Maintain a sufficient migration period
    ↓
Remove it in a major version or new language mode

A deprecated feature is not recommended for use, but that does not mean it immediately becomes unavailable. A deprecation warning should preferably include an alternative and the planned time of removal.

If features are retained permanently without removal, the complexity of the language and implementation continues to increase. Conversely, removing them too quickly can prevent the ecosystem from moving to a new version. Usage, security risk, maintenance cost, and the possibility of automated migration must therefore be considered together.

Language Versions and Editions

How language changes are grouped and distributed is also an important design issue.

Common approaches include the following.

  • Release new language versions at regular intervals.
  • Continuously incorporate completed features into the specification.
  • Provide separately maintained major versions with long-term support.
  • Allow the source file or project to select a language version.
  • Support different language editions in the same compiler.

Rust places some backward-incompatible changes within opt-in boundaries called editions. A project retains the rules of its current edition unless it explicitly selects a new one, and libraries using different editions can coexist in the same dependency graph. Rust provides automated migration tools to transform source code for a new edition.

This approach makes it possible to introduce syntactically incompatible changes without dividing the entire language into a completely separate new version. However, compilers and tools must simultaneously manage the grammatical and semantic differences among multiple editions.

ECMAScript incorporates completed proposals into a living current specification and publishes it as regular annual editions.

Release Cycles of Standards and Implementations

The revision cycle of a standards document and the release cycle of a compiler may differ. An implementation may experimentally provide a feature not yet included in the final standard or may gradually implement features after a new standard is published.

The following states must therefore be distinguished.

  • A feature that exists only as a proposal and has not been implemented
  • A feature for which only an experimental implementation exists
  • A feature included in the specification but unsupported by some implementations
  • An extension supported by major implementations but not included in the formal standard
  • A feature stably supported by both the standard and major implementations

Users must check not only the language version but also the implementation version, target platform, and enabled feature options. Even when implementations report the same language version, their supported scope and defects may differ.

Standard Extensions

Actual implementations may provide extensions not included in the standard. Hardware-specific vector types and attributes, inline assembly, operating-system-specific APIs, and compiler intrinsics are representative examples.

Extensions make it possible to use hardware and platform features that are difficult to express through the standard alone. However, programs depending on extensions may become difficult to port to other compilers and platforms.

Implementations can manage extensions in the following ways.

  • Use a separate namespace or prefix.
  • Require extensions to be enabled explicitly.
  • Diagnose the use of extensions in standards mode.
  • Provide feature-detection macros or attributes.
  • Propose widely adopted extensions to the standardization committee.

When similar extensions are independently provided by multiple implementations, they may become candidates for standardization. Conversely, a feature excessively dependent on the internal structure of one implementation may be difficult to turn into a common standard.

Feature Detection

Programs supporting multiple language versions and implementations need a way to determine whether a particular feature is available.

Simply checking the compiler version number may fail to handle forked implementations or independently backported features accurately. A language may therefore provide the following forms of feature detection.

  • Checking whether a particular syntax feature is supported
  • Checking whether a standard-library API exists
  • Checking whether attributes and built-in functions are supported
  • Checking the target operating system and CPU features
  • Selecting a language edition or feature set
  • Conditional compilation

Feature detection can increase program complexity by adding conditional branches. It is therefore preferable to establish a commonly supported minimum version and handle only necessary differences in a limited way.

Specification Defects and Interpretation

Even a highly detailed specification may contain ambiguous wording, contradictions, and omissions. Problems in the specification may be discovered while implementations are being developed or new use cases arise.

Specification defects may appear in forms such as the following.

  • Grammar can be interpreted in two different ways.
  • Rules in different clauses conflict.
  • The result of a particular boundary condition is undefined.
  • Examples and normative wording do not match.
  • A requirement is impossible or excessively inefficient to implement.
  • Common behavior among existing implementations differs from the specification.

Standards organizations and language projects receive defect reports and revise the specification after examining its intent, existing implementations, and compatibility. Simple editorial corrections may be incorporated quickly, but changes to program meaning may require review similar to a language-change proposal.

When correcting a specification defect, it is also necessary to consider which interpretation existing programs depend on. Even when changing to the theoretically correct rule, a staged migration process may be necessary if widely used programs would be broken.

Implementation Experience and Empirical Design

Language features should be evaluated not only for theoretical consistency but also for usability in actual code. Prototype compilers, experimental branches, and external libraries are ways to accumulate experience before including a feature in the formal language.

Empirical design can use materials such as the following.

  • Repeated patterns in real codebases
  • Frequently occurring compilation errors
  • Length and complexity of code using the feature
  • Compilation time and execution performance
  • Generated binary size
  • Implementation difficulty for IDEs and static analyzers
  • Learning outcomes for new users
  • Comparison with alternatives written using existing features
  • Results of automated transformations applied to open-source projects

A feature may appear concise in a few examples but produce complex error messages and inference results in a large codebase. Conversely, a feature that initially appears limited may solve a common problem across many fields.

Past revisions of the Java Language Specification have also used actual open-source code collections to measure the effects of type-inference approaches, demonstrating the use of real-code analysis in changes to mature languages.

Security and Safety

Language design must consider not only expressiveness and performance but also whether the language makes security vulnerabilities easy to create.

Major areas of review include the following.

  • Out-of-bounds array access
  • Use of freed memory and invalid pointers
  • Integer overflow and truncation
  • Incorrect combination of strings and commands
  • Use of uninitialized values
  • Data races
  • Unsafe deserialization
  • Privilege and trust boundaries
  • Exceptions and resource cleanup
  • Hygiene of code generation and macros
  • Supply-chain risks and executable package code

Not every aspect of safety can be guaranteed at the language level. However, safe defaults and explicit boundaries around dangerous operations can reduce the area in which defects may occur.

Safety features also affect execution costs and implementation complexity. Bounds checks, overflow checks, and runtime type checks can introduce additional costs, so semantic rules and intermediate representations should be designed to allow the compiler to eliminate unnecessary checks.

Portability

Portability is the property that allows a program to be used with little modification across different implementations, operating systems, and processors. Portability is also a central objective of C standardization and the activities of SC 22.

The following must be considered to improve portability.

  • Sizes and ranges of primitive types
  • Integer and floating-point operations
  • Endianness and alignment
  • File paths and character encoding
  • Threads and memory models
  • Differences in operating-system APIs
  • Time and locale
  • Networking and file systems
  • External libraries and ABIs
  • Conditional compilation

It is difficult to hide every difference among platforms completely, and excessive abstraction may make platform-specific capabilities difficult to use. A language and its standard library should provide common functionality while allowing access to platform-specific extensions when necessary.

Language Stability and Evolution

A programming language that does not change may find it difficult to respond to new hardware, development methods, and safety requirements. Conversely, when a language changes too rapidly, users must continually modify their code and libraries and tools may struggle to become stable.

Stable language development requires the following elements.

  • Clear design goals
  • Documented change procedures
  • Public proposals and review
  • Actual implementation and usage experience
  • Conformance testing
  • Backward-compatibility policy
  • Deprecation and migration procedures
  • Automated code-transformation tools
  • Continuous synchronization between specifications and implementations
  • Responsible governance

The decision not to add a feature can be as important as adding one. Once syntax, semantics, and APIs are standardized, they must be maintained for a long period, so the consistency and maintenance cost of the entire language must be considered over short-term convenience.

Standardization is also not a process of freezing a language and preventing change. It is a process of maintaining an agreed common foundation while reviewing new requirements and balancing the stability of existing programs with the possibility of future development.

Therefore, the design and standardization of a programming language are both technical work involving the selection of syntax and features and institutional work involving the creation of rules and decision-making structures that allow multiple implementations, users, libraries, and tools to develop together over long periods. The success of a language is determined less by the number of individual features than by how stably its design principles, specification accuracy, implementation quality, compatibility policy, and ecosystem are connected.

Fields of Application

Programming languages are used in almost every field that involves computers, ranging from operating systems and device control to web services, mobile applications, games, scientific computing, artificial intelligence, and business automation. However, not every language is equally suitable for every field. The actual range of use depends on how well a language’s execution model, type system, memory-management method, supported platforms, standard library, development tools, and existing ecosystem match the requirements of a particular field.

The fields in which a programming language is used are not determined solely by the features of the language itself. The same language may become widely used in a particular field through its close integration with a specific operating system, framework, runtime, or industrial ecosystem. Kotlin is a general-purpose programming language, but it is closely integrated with the Android development environment, while Swift is used centrally for application development on Apple platforms. JavaScript plays an important role in web frontend development because it is the standard execution language of web browsers. Google operates Android development with Kotlin as a primary language, while Apple provides Swift as a language for Apple platforms including iOS, iPadOS, macOS, tvOS, and watchOS.

A single software system may also be composed of multiple languages. A game may implement its engine and performance-critical systems in C++, while game rules or tools are written in C#, Lua, or visual scripting. A web service may use JavaScript or TypeScript in the browser, Java, C#, Go, Python, or Rust on the server, and SQL for database queries. A machine-learning system may define models and training procedures in Python while implementing computationally intensive operations in C++, CUDA, or another language targeting accelerators.

Therefore, when examining fields of application, it is more appropriate to consider both the requirements of the field and the role performed by the language rather than assigning each language to a single fixed field.

System Software

System software is software that provides the execution foundation for other programs, including operating systems, device drivers, compilers, runtimes, file systems, database engines, network stacks, and virtual machines.

The following characteristics are important in system software.

  • Memory layout and lifetimes must be controllable in detail.
  • The software must be able to access operating-system and hardware interfaces directly.
  • Execution time and memory usage must be predictable.
  • It should be possible to run without a separate large-scale runtime.
  • Multiple CPU architectures and ABIs must be supported.
  • External libraries and operating-system APIs must be callable reliably.
  • Performance and compatibility must be preserved in code maintained over long periods.

C (programming language) has long been used in operating systems, system libraries, and embedded software because it provides pointers, structures, explicit memory management, a relatively simple execution model, and widely supported ABIs. The C standard defines the representation, syntax, constraints, semantic rules, and limitations that conforming implementations of C programs must follow.

C++ retains hardware control and native execution performance close to C while providing classes, templates, generic programming, scope-based resource management, and a standard library. It is used in systems that require both performance and complex abstractions, including browsers, game engines, databases, compilers, and large desktop applications.

Rust is a systems programming language that seeks to provide memory safety without a garbage collector through ownership, borrowing, and lifetime checking. Command-line programs, network services, WebAssembly, and embedded devices have been treated as major application areas of the Rust project.

Systems programming does not require every part of a system to be written in a low-level language. The core operating-system components and hardware boundaries may be implemented in native languages, while administration tools, installers, build systems, and test automation are written in Python or shell languages. Languages at different levels are often combined to improve the reliability and development efficiency of the overall system.

Embedded Systems

Embedded systems are computer systems built into particular devices or products to perform limited purposes. They include microcontrollers, sensors, automotive controllers, industrial equipment, household appliances, medical devices, drones, communication devices, and Internet of Things devices.

Embedded environments vary greatly by device. Some devices use 8-bit microcontrollers with only a few kilobytes of memory, while others have multicore processors, operating systems, and several gigabytes of memory. The Embedded Rust documentation also explains that embedded programming ranges from small 8-bit MCUs to multicore systems and that different constraints apply depending on the target.

The following factors are important in embedded software.

  • Limited RAM and storage
  • Low power consumption
  • Control of hardware registers and peripherals
  • Interrupt handling
  • Real-time response
  • Bare-metal execution without an operating system
  • Long-term stability
  • Remote updates and failure recovery
  • Safety and security
  • Cross-compilation for target devices

C and C++ are used in embedded environments based on widely supported compilers, device libraries, and hardware-vendor SDKs. Rust also supports no_std environments without an operating system or standard library, allowing ownership and type-safety features to be used on microcontrollers. The Embedded Rust Book officially explains how to use Rust on bare-metal microcontrollers.

Concurrency in embedded environments does not appear only as operating-system threads. Interrupt handlers may execute at unpredictable times, real-time tasks may alternate, and multiple cores may execute code independently. Such execution structures require shared state, atomic access, interrupt priorities, and real-time scheduling to be considered together.

Real-Time Systems

Real-time systems are not merely systems that execute quickly, but systems that are considered correct only if they produce results within a specified time. Factory control, robotics, aircraft control, vehicle control, communication devices, medical devices, and audio processing are representative application areas.

In real-time systems, upper bounds on worst-case execution time and latency, task priorities, interrupt response, resource contention, and scheduling predictability are more important than average processing speed. Garbage collection or runtime compilation capable of suspending execution for arbitrary periods may be unsuitable for some real-time environments, so restricted real-time runtimes or predictable memory-management methods are used.

Ada evolved to support systems requiring tasks, real-time execution, a strong type system, and high integrity. The high-integrity systems annex of the Ada standard addresses restrictions and execution environments for systems requiring high reliability and safety.

A real-time program also does not need to be written entirely in one language. Control loops with strict timing constraints may be implemented in C, C++, Ada, or Rust, while configuration tools, data analysis, simulation, and user interfaces are implemented in other high-level languages.

Desktop Applications

Desktop applications are programs that run directly on personal-computer operating systems such as Windows, macOS, and Linux. They include document editors, graphics tools, development environments, messaging applications, media programs, design tools, file managers, and enterprise clients.

The following requirements are important in desktop applications.

  • Integration with operating-system windows, input, files, and notifications
  • Complex graphical user interfaces
  • Access to local files and devices
  • Offline execution
  • Installation and automatic updates
  • Support for multiple operating systems
  • Accessibility and internationalization
  • Stability during long-running execution
  • Native performance and memory usage

Native Windows programs may use C++, C#, and .NET, while macOS programs may use Swift, Objective-C, and C++. Programs supporting multiple operating systems may use C++ and Qt, C#-based cross-platform frameworks, Java and the JVM, JavaScript-based desktop runtimes, or Rust-based GUI tools.

Python can also be used in several development areas, including desktop applications, scripting, web services, and scientific computing, and can combine GUI libraries with native extension modules.

Even when the entire user interface of a desktop program is written in one language, its internal computation engine may consist of libraries written in another language. Video editors and 3D content-creation tools often separate their UI and scripting layers from high-performance media and graphics-processing layers.

Mobile Applications

Mobile applications run on smartphones, tablets, and wearable devices and must account for touch input, sensors, cameras, notifications, location information, limited power, and background-execution policies.

Kotlin and Java are major languages on Android. Google recommends Kotlin as the preferred language for Android development and supports its integration with existing Java code.

Kotlin coroutines are used to express asynchronous work that must not block the UI thread, such as network requests and database access. The official Android documentation describes coroutines as a recommended approach for asynchronous programming and presents structured concurrency and cancellation propagation as major characteristics.

Swift and Objective-C are used on Apple platforms. Apple provides Swift as a language for all Apple platforms, while SwiftUI is a framework for building user interfaces across multiple Apple devices using Swift.

Cross-platform mobile development can use frameworks that support both Android and iOS from one codebase. Kotlin Multiplatform supports sharing business logic between Android and iOS and can be combined with platform-specific UIs or shared UIs as needed.

Mobile games and high-performance media applications may connect a common engine written in C++ to platform-specific Kotlin and Java layers or Swift and Objective-C layers.

Web Frontend

Web frontend refers to the area of software that executes in a web browser and handles document presentation, user input, network communication, animation, and multimedia.

The fundamental web technologies are divided by role.

  • HTML represents the structure and meaning of a document.
  • CSS defines the appearance and layout of a document.
  • JavaScript handles user input, dynamic state changes, and the use of browser APIs.

JavaScript is a programming language that adds dynamic content updates, graphics, multimedia control, and interaction to web pages. Browsers provide JavaScript with various Web APIs for the document object model, networking, storage, graphics, audio, and other capabilities.

TypeScript adds static type annotations and analysis to JavaScript and is transformed into ordinary JavaScript. In large frontend programs, it can assist code navigation, refactoring, and error detection by describing the types of component properties, network data, and state-management structures.

Web frontend software is constrained by the browser environment. Access to files, devices, and networks must follow the browser security model and user permissions, while differences in browser feature support, performance, and accessibility must be considered.

WebAssembly can be used as a target for executing low-level code generated from languages such as C, C++, and Rust in web environments. Compute-intensive modules for image and audio processing, games, simulation, and compression can be implemented in WebAssembly, while JavaScript manages the browser UI and API connections.

Web Backend and Server Applications

Web backend refers to server-side software that receives client requests and performs authentication, business logic, database access, file processing, and communication with external services.

The following elements are important in web servers and APIs.

  • Handling many concurrent connections
  • Network input/output and asynchronous execution
  • Database connectivity
  • Authentication and authorization
  • Validation of input data
  • Horizontal scaling
  • Observability and logging
  • Security updates
  • Reliable deployment
  • Integration with existing services and libraries

Java and Kotlin can use JVM server frameworks and enterprise ecosystems, while C# is combined with .NET web and cloud frameworks. Python can be used for web APIs, administration services, and data-oriented backends based on rapid development and its broad library ecosystem. JavaScript and TypeScript can share languages and data structures between browsers and servers through server runtimes such as Node.js.

Go is used for network services and cloud tools based on goroutines, channels, networking and HTTP standard libraries, and native deployment. Rust can be applied to services requiring asynchronous runtimes, memory safety, and native performance. Network services have also been treated as a major application area by the Rust project.

The choice of a server language is not determined solely by the execution performance of one request. Development speed, failure response, library maturity, deployment environment, memory usage, startup time, operator experience, and compatibility with existing systems also have an influence.

Cloud Computing

In Cloud computing environments, programs run on virtual machines, containers, serverless execution environments, managed data services, and distributed infrastructure across multiple regions.

Cloud software can be divided into areas such as the following.

  • Infrastructure control and resource provisioning
  • Container and cluster management
  • Microservices and APIs
  • Serverless functions
  • Log and metric collection
  • Data-processing pipelines
  • Deployment and operations automation
  • Security policies and access control

Go is used in cloud infrastructure tools because of its single executable files, relatively fast startup, concurrency, and network-programming features. Java and C# are widely used in enterprise systems and managed cloud services, while Python can be used for automation through cloud APIs, data processing, and serverless functions. Rust and C++ can be applied to infrastructure software requiring high throughput and low latency, such as proxies, storage engines, and network components.

Cloud programs are affected by the runtime characteristics and operating costs of a language. Memory usage and startup time can influence costs in serverless environments and large-scale container deployments, while garbage collection and JIT compilation exhibit different characteristics in long-running services and short-lived functions.

Distributed Systems

Distributed systems are systems in which multiple computers with independent memory and independent possibilities of failure form one service through a network.

The following problems are important in distributed systems.

  • Network latency and disconnection
  • Message duplication and reordering
  • Partial failures in which only some nodes fail
  • Data replication and consistency
  • Task retries
  • Uncertainty in time and ordering
  • Failure detection
  • Service discovery
  • Load balancing
  • Communication between services of different versions

When a language provides message passing, actors, asynchronous functions, lightweight tasks, and structured concurrency, some structures of these systems can be expressed more directly. However, language features alone cannot automatically solve distributed consensus, data consistency, and network-partition problems. Data-storage methods, communication protocols, retry policies, and failure models must be designed separately.

Various languages are used for distributed services, including Java, C#, Go, Rust, Erlang, Elixir, Scala, C++, and Python. Erlang-family languages center on isolated processes, message passing, and supervision structures, while JVM- and .NET-based languages use mature server and observability ecosystems.

Command-Line Programs

Command-line programs run in terminals or shells, receive arguments and standard input, and provide results through standard output or files. They include compilers, version-control tools, file converters, package managers, build tools, and system-administration programs.

The following characteristics are important in command-line programs.

  • Fast startup time
  • Clear arguments and help text
  • Composition using pipes and files
  • Exit codes suitable for automation
  • Distribution across multiple operating systems
  • Small executables and few dependencies
  • Error messages and logs
  • Shell completion

Shell languages are suitable for quickly connecting operating-system commands and pipelines, but as programs grow, limitations in types, data structures, and error handling may appear. Python can process command-line arguments using facilities such as the standard library's argparse, while Rust officially treats CLI development using native executables and its package ecosystem as an application area.

Go, C++, and C# are also used for standalone administration and development tools. The suitable language depends on the installation process, target platforms, startup time, and whether native libraries are used.

Scripts and Business Automation

A Script refers to a relatively small program written to automate file processing, program execution, data transformation, system configuration, and repetitive work.

The following features are important in automation.

  • String and file processing
  • Execution of operating-system commands
  • Processing of JSON, XML, and CSV
  • HTTP requests and API calls
  • Rapid modification and execution
  • Connection to external programs
  • Error and log handling
  • Support for multiple operating systems

Shell languages are suitable for direct control of files, processes, and pipelines. Python can construct file, network, serialization, testing, and command-line tools using its standard library and external packages. PowerShell provides object-based pipelines and a Windows administration environment, while JavaScript is used in Node.js-based tools and web-development automation.

A short one-time task may require only a simple script, but when an automation program becomes responsible for deployment, security, or important data, it requires the same level of type checking, testing, error handling, permission management, and maintenance as an ordinary application.

DevOps and Build Automation

In DevOps and software-delivery environments, the processes of building, testing, packaging, and deploying source code are automated.

Programs used in these environments include the following.

  • Build scripts
  • Continuous-integration pipelines
  • Container-image configuration
  • Infrastructure definitions
  • Deployment and rollback tools
  • Test automation
  • Log and state collection
  • Development-environment configuration

Shell, Python, Go, JavaScript, Ruby, and PowerShell are used to write automation tools. Some build and infrastructure systems provide declarative formats such as YAML or their own domain-specific languages.

Reproducibility, idempotence, secret management, failure termination and recovery, and execution logs are important in automation environments. Rather than choosing a language merely because its syntax is concise, it is necessary to consider whether failures can be detected accurately and environmental differences can be controlled.

Databases and Data Management

In databases, languages used to implement storage engines are distinguished from languages used to query stored data.

The internal engine of a database management system performs functions such as the following.

  • Management of disk and memory pages
  • Indexes
  • Query optimization
  • Transactions
  • Concurrency control
  • Logging and recovery
  • Replication
  • Network protocols

Because these components require detailed control over performance, memory, and concurrency, languages such as C, C++, Rust, and Java may be used.

Users describe the conditions and form of desired data using declarative languages such as SQL. The actual selection of indexes, join order, and parallel-processing method is determined by the database query optimizer.

Applications surrounding a database use various languages, including Java, C#, Python, JavaScript, Go, and PHP, to execute SQL and convert results into domain objects. Object-relational mapping tools may be used, but they cannot completely abstract database transactions, indexes, and query-execution plans.

Data Engineering

Data engineering is the field of collecting, cleaning, and transforming data from multiple sources into forms usable for analysis and services.

Major tasks include the following.

  • Data ingestion
  • Batch and stream processing
  • Schema transformation
  • Data-quality checks
  • Movement between data stores
  • Job scheduling
  • Distributed processing
  • Metadata management
  • Creation of analytical data models

Python and SQL are used to construct data-processing procedures and analytical systems, while Java and Scala are used in JVM-based distributed data-processing systems. Azure Databricks likewise supports several programming languages for data-engineering and data-science work and explains that the available locations and limitations differ by language.

In data engineering, the performance of an individual operation in the language may have less influence on total performance than the distributed execution engine, storage format, network costs, data layout, and partitioning. Work written in a high-level language may internally execute on the JVM, in C++, or through native vector operations.

Data Analysis and Statistics

Data analysis is the field of exploring and summarizing data, finding statistical relationships, and visualizing results.

Python and R (programming language) provide ecosystems for interactive analysis, statistics, and data visualization. SQL is used to select and aggregate data for analysis, while languages and environments such as Julia and MATLAB are also used in numerical computing and research.

The following characteristics are important in analytical languages.

  • Processing of arrays and tabular data
  • Statistical functions
  • Visualization
  • Interactive execution environments
  • Notebooks
  • Connections to scientific libraries
  • Calling high-performance libraries written in C, C++, and Fortran
  • Reproducibility of analytical results

Interactive analysis code is suitable for rapid experimentation, but when deployed to an operational system, input validation, testing, version pinning, resource usage, and failure handling must be strengthened.

Artificial Intelligence and Machine Learning

The fields of Artificial intelligence and Machine learning involve data preparation, model definition, training, evaluation, inference, and service deployment.

Python is used as a high-level interface for model research, experimentation, and training pipelines because of its concise expression and data and numerical-computing ecosystem. However, large-scale matrix operations, tensor processing, and GPU kernels are generally performed by native implementations in C++, CUDA, or other languages. Python code may control these high-performance libraries and runtimes.

CUDA is a parallel-computing platform and programming model for accelerating compute-intensive programs using GPUs. NVIDIA's official CUDA documentation explains that GPU computing is used in deep learning, scientific computing, and high-performance computing and covers GPU programming through C++ and Python.

A machine-learning system may consist of several layers as follows.

Experimentation and model definition
    Python

Framework and execution engine
    C++ / Rust / Java

GPU computation
    CUDA / Shader and kernel languages

Data processing
    Python / SQL / Scala / Java

Service API
    Python / Go / Java / C# / Rust

Mobile and edge inference
    C++ / Java / Kotlin / Swift

The choice of language does not directly determine model accuracy. Supported frameworks, hardware accelerators, data-processing environments, deployment targets, and operational tools have a more direct influence.

Scientific Computing

Scientific computing is the field of computing mathematical models in physics, chemistry, weather, fluid mechanics, astronomy, life sciences, numerical analysis, and engineering simulation.

The following elements are important in scientific computing.

  • Accuracy and reproducibility of floating-point calculations
  • Array and matrix operations
  • Numerical algorithms
  • Parallel processing
  • Large-scale memory and storage
  • Compatibility with existing research code
  • Maintenance of code across multiple generations
  • Interoperability with other languages and libraries

Fortran has been used particularly in fields where computers were adopted early in science and engineering. Fortran-lang describes weather and ocean prediction, computational fluid dynamics, applied mathematics, statistics, and finance as major application areas of Fortran.

C, C++, Python, Julia, MATLAB, and other languages are also used in scientific computing. Python is used as a high-level layer for organizing research procedures and data analysis, while actual computations may be delegated to libraries written in Fortran, C, or C++.

In scientific software, the value of already verified algorithms, data formats, and code accumulated over long periods may be greater than that of the latest syntax. When introducing a new language, connections with existing code, result verification, and differences in floating-point behavior must be considered.

High-Performance Computing

High-performance computing is the field of performing large-scale computations using supercomputers, computing clusters, multicore CPUs, and GPUs. It is used in weather prediction, physical simulation, genomic analysis, fluid dynamics, materials research, financial computation, and machine-learning training.

The following levels of parallelization may coexist in high-performance computing.

  • Vectorization at the CPU instruction level
  • Multithreading within a single node
  • Message passing between multiple nodes
  • GPU and accelerator offloading
  • Heterogeneous execution combining multiple computing devices

Fortran, C, and C++ have traditionally been used, while Python and Julia may be used for high-level workflow organization and analysis. CUDA C++, kernel languages, and accelerator libraries are used in GPU computing. CUDA's programming model provides functions for writing code to execute on GPUs and controlling memory and parallel-execution structures.

Performance in high-performance computing is not determined solely by the theoretical execution speed of a language. Memory-access patterns, data layout, communication volume, vectorization, the parallel structure of algorithms, and optimization for the target device are important.

Computer Graphics

The field of Computer graphics implements rendering of 2D and 3D scenes, image processing, modeling, animation, simulation, and user interfaces.

Graphics software is generally composed of the following layers.

  • Graphics APIs and device abstraction
  • Rendering engines
  • Shaders
  • Scene and asset processing
  • Editors and tools
  • Animation and physics integration
  • Content scripting

Rendering engines and graphics drivers often use C and C++ for hardware access and performance control. Rust can also be used in graphics API wrappers and rendering engines.

Shaders are written in domain-specific languages targeting GPU vertex, pixel, and compute stages. GLSL, HLSL, and Metal Shading Language are representative examples. Shader code must account for massively parallel execution models and memory structures different from ordinary CPU programs.

In graphics-production tools and pipelines, Python can be used for asset conversion, automation of repetitive tasks, plugins, and production tools. Therefore, a single graphics program may use a native renderer, shader languages, and tool scripting together.

Game Development

Game development is a complex field that combines real-time rendering, physics, audio, networking, artificial intelligence, input, user interfaces, tools, and content production.

Different languages may be used according to their roles in game development.

  • Game engines and renderers
  • Physics and audio systems
  • Gameplay code
  • Servers and networking
  • Editors and production tools
  • Build and asset pipelines
  • Shaders
  • Mods and user scripting

C++ is used in large game engines based on high performance, memory control, and support for consoles and graphics APIs. Unreal Engine provides frameworks and APIs for C++ developers, while game systems and content can be constructed using both C++ and Blueprint visual scripting.

C# is used in some game engines and tools based on managed runtimes and productivity. Lua, Python, and custom scripting languages may be embedded in native engines for game rules, content, mods, or editor automation.

Language choice in games is not only a matter of maximum frame performance. Real-time code and tool code have different requirements. Low latency and predictable memory usage are important in the core paths of rendering and physics, while rapid development, UI, and file-processing capabilities may be more important in editors and build pipelines.

Audio and Media Processing

Audio and video programs process large data streams in real time or non-real time. They include codecs, mixers, signal processing, video conversion, editing, streaming, and playback systems.

In real-time audio, missing a processing deadline can cause sound interruptions, so predictable execution and restrictions on memory allocation are important. C and C++ are used in DSP, codecs, and audio engines, while Rust can also be applied to native media processing requiring memory safety.

Media-production programs may combine native processing engines with Python- or JavaScript-based automation and plugin layers. CUDA and compute shaders can be used for GPU-based video processing.

Network Programming

Network programming implements sockets, protocols, servers and clients, proxies, network devices, and communication libraries.

The following elements are important in network programs.

  • Many concurrent connections
  • Asynchronous input/output
  • Timeouts and cancellation
  • Message boundaries and serialization
  • Security protocols
  • Partial sending and receiving
  • Connection closure and reconnection
  • Buffer and memory management
  • Operating-system-specific network APIs

C and C++ are used in operating-system network stacks, high-performance proxies, and protocol implementations. Go is suitable for service development using goroutines and its networking standard library, while Rust can use memory safety and asynchronous libraries. Java, C#, Python, and JavaScript are also used in server and client applications.

Asynchronous syntax provided by a language can reduce complexity in network programs, but protocol state, time limits, input validation, and defenses against attacks must be designed separately.

Cybersecurity

The field of Cybersecurity includes security-product development, vulnerability analysis, network monitoring, cryptographic implementation, malware analysis, digital forensics, and automation.

The languages used differ according to the task.

  • C and C++ are used for operating systems, binaries, networking, and low-level security tools.
  • Rust can be used for security tools and system components where memory safety is important.
  • Python is used for analysis, automation, security API integration, and testing tools.
  • Assembly language may be required to analyze executables and processor instructions.
  • JavaScript is used in web security and analysis of browser environments.
  • SQL is relevant to database security and query analysis.

Using a safe language in security does not make the entire program automatically secure. Authentication, authorization, cryptographic protocols, input validation, key management, and supply-chain security are separate concerns from the memory safety of a language.

Cryptography

Cryptography software implements encryption, signatures, hashes, key exchange, and random-number generation algorithms.

The following factors are important in cryptographic implementations.

  • Exact integer and bit operations
  • Constant-time execution
  • Memory management of secret information
  • Code changes caused by optimization
  • Hardware-acceleration instructions
  • Use of verified algorithms
  • Strict validation of external input
  • Independent review of implementations

High-performance cryptographic libraries may combine C, C++, Rust, and assembly language. Applications written in Python, Java, C#, and other languages generally call verified standard libraries and platform APIs rather than implementing cryptographic algorithms directly.

Finance and Business Systems

Finance, insurance, government, retail, accounting, and enterprise systems process transactions, customers, contracts, payments, ledgers, and reports.

The following elements are important in these fields.

  • Data accuracy
  • Transactions and audit records
  • Long-term compatibility
  • Access control and regulatory compliance
  • Large-scale data processing
  • Integration with existing systems
  • Failure recovery
  • Precise monetary calculations

COBOL has been used in existing business systems for banks, insurance companies, governments, and large enterprises. Java, C#, and SQL are used in modern enterprise servers and business applications, while JavaScript and TypeScript are used in user interfaces.

Languages such as C++, Java, Rust, and Fortran may be used in fields where latency and throughput are important, including high-frequency trading and risk calculation. Fortran includes statistics and finance among its application areas in scientific and numerical computing.

Changing the language of a financial system is not merely a code-conversion problem. Existing transaction rules, data formats, regulations, and behavior accumulated over decades must be preserved accurately, making gradual modernization and interoperability important.

Safety-Critical Systems

Safety-critical systems are systems in which software failure may lead to loss of human life or health, environmental damage, or large-scale property loss. They include aviation, railways, automobiles, medical devices, industrial control, and nuclear systems.

These fields require stricter development and verification than ordinary applications.

  • Traceability of requirements
  • Coding rules
  • Static analysis
  • Measurement of test coverage
  • Formal verification
  • Worst-case execution-time analysis
  • Restricted use of dynamic memory
  • Certifiable tools
  • Change management and audit records

Ada can be used for systems requiring certification under high-integrity application and safety standards. Ada's strong type system and restricted execution profiles were designed to reduce the possibility of program errors and support verification.

C and C++ are also used in automotive, aviation, and industrial environments, but safe subsets, coding standards, static analysis, and runtime restrictions may be applied together. The development process, verification tools, hardware, and overall system safety design may be more important than the choice of language alone.

Robotics and Autonomous Systems

Robotics and autonomous systems combine sensor input, localization, planning, control, image processing, and hardware communication.

The major roles of languages can be divided as follows.

  • C and C++ are used for real-time control, sensor processing, and robotics middleware.
  • Python is used for research algorithms, experimentation, tools, and integration with machine-learning models.
  • CUDA and GPU languages accelerate image and neural-network processing.
  • Rust can be applied to safe device communication and system components.
  • MATLAB- and Simulink-family environments can be used for control models and simulation.

Because robotic programs interact directly with the physical world, errors may lead not only to program termination but also to dangerous device behavior. Real-time constraints, sensor uncertainty, emergency stopping, and safe states must be addressed together with system design outside the language itself.

Internet of Things

The Internet of Things consists of systems in which sensors and devices are connected through networks to collect data and perform remote commands.

A single IoT system may consist of several layers.

Device firmware
    C / C++ / Rust

Gateway
    C++ / Rust / Go / Python

Message processing and servers
    Go / Java / C# / Python / JavaScript

Data storage and analysis
    SQL / Python / Scala

Management interface
    JavaScript / TypeScript

On devices, limited memory and power, network disconnection, and reliability of remote updates are important. On servers, handling connections and messages from many devices, device authentication, and data analysis are important.

IoT devices may remain installed in the field for long periods and be difficult to maintain directly, so secure updates, key management, communication encryption, and failure recovery are as important as language selection.

Blockchain and Distributed Ledgers

Blockchain and distributed-ledger systems consist of node programs, consensus protocols, network communication, storage, and smart contracts.

C++, Go, Rust, Java, and other languages may be used for node implementations, while platform-specific domain-specific languages or restricted general-purpose languages are used for smart contracts.

Because smart contracts are difficult to modify after deployment and execution costs and state changes can be directly tied to economic value, the following factors are important.

  • Deterministic execution
  • Integer and monetary-value handling
  • Permission checks
  • Reentrancy and call relationships
  • Resource-use limits
  • Atomicity of state changes
  • Formal verification and static analysis
  • Upgrade policies

A defect recoverable in an ordinary server program can cause permanent asset loss in a smart contract, so restrictions and verifiability may be prioritized over language expressiveness.

Education

Programming languages are used to teach computer science, software development, logical thinking, and problem solving.

The following factors are considered when choosing a language for education.

  • Simplicity and consistency of syntax
  • Error messages
  • Ease of installation and execution
  • Interactive practice
  • Visualization and debugging
  • Suitability for the concepts being taught
  • Connection to languages used later
  • Textbooks and educational materials
  • Accessibility across operating systems and devices

Python can be used in introductory education and data and science education based on its relatively concise syntax and interactive execution environment. Swift Playgrounds provides interactive lessons using actual Swift code for learning coding and application development.

Scratch and block-based languages reduce the burden of syntax errors and allow visual learning of events, iteration, conditions, and variables. University computer-science education may use different languages according to educational goals, including Python, Java, C, C++, Haskell, and Scheme.

A language that is easy to begin with is not necessarily the most suitable for teaching every advanced concept. C and assembly language can help in learning memory and operating systems, while Haskell or ML-family languages may be suitable for learning functions and type theory.

Programming-Language Research

Programming languages themselves are also subjects of research. New syntax, type systems, semantics, compiler optimizations, program verification, and development tools are studied.

Major research fields include the following.

  • Type theory
  • Formal semantics
  • Compilers
  • Static analysis
  • Program verification
  • Memory safety
  • Concurrency models
  • Domain-specific languages
  • Synthesis and code generation
  • Languages and user experience
  • Error messages and development tools
  • Quantum programming languages
  • Probabilistic programming languages

Research languages can experiment with new features that are difficult to provide in industrial general-purpose languages. Verified features may later be absorbed into existing languages, libraries, and compilers.

A feature successful in research is not immediately suitable for an industrial language. Implementation costs, compilation time, error diagnostics, compatibility with existing code, and user learning costs must also be reviewed.

Domain-Specific Languages

A Domain-specific language is a language designed to express the concepts and rules of a particular problem domain directly.

Representative examples include the following.

  • SQL expresses relational data queries.
  • Regular expressions express string patterns.
  • Shader languages express GPU graphics operations.
  • Hardware-description languages express digital circuits.
  • Build languages express dependencies between files and tasks.
  • Configuration languages express the configuration of programs and systems.
  • Statistical-modeling languages express probability distributions and observation relationships.
  • Policy languages express permission and access rules.

A domain-specific language can provide shorter and clearer expressions than a general-purpose language in its field, while its execution engine can use domain knowledge for optimization and verification. However, its range of application is narrow, and separate tools and learning may be required.

A domain-specific language may exist as an independent file format or may be embedded within the libraries, operators, and macros of a general-purpose language.

Hardware Design

The design of digital circuits and processors requires a computational model different from sequential software execution. Multiple parts of a circuit operate simultaneously, and signals, clocks, delays, and physical resource structures must be represented.

Hardware-description languages such as Verilog and VHDL describe registers, combinational circuits, state machines, and module connections. A written design can be simulated or synthesized into FPGA and ASIC circuits.

More recently, hardware-construction languages built on general-purpose languages such as Scala, Python, and Haskell have also been used to generate circuit structures. In this case, the abstraction and metaprogramming features of the general-purpose language are used to generate a hardware-description intermediate representation.

Shaders and GPU Programming

GPUs are designed to perform similar operations in parallel across many computational units. Shaders and GPU kernels target this execution model.

Graphics shaders perform tasks such as the following.

  • Vertex-coordinate transformation
  • Surface material and lighting calculations
  • Texture sampling
  • Pixel-color calculation
  • Post-processing
  • Ray tracing
  • General-purpose parallel computation

GPU languages must account for threads, workgroups, memory hierarchies, synchronization, and branch divergence. Even when they use syntax similar to ordinary CPU languages, their execution model differs greatly.

CUDA provides a model for using GPU computation from C++ and Python, while graphics APIs can use shader representations such as HLSL, GLSL, and SPIR-V.

Quantum Computing

In Quantum computing, languages and SDKs are used to express qubits, quantum gates, measurement, and classical control.

A quantum program generally has a hybrid structure in which a classical program constructs a quantum circuit, receives execution results, and performs post-processing.

Classical host program
    ↓
Quantum-circuit generation
    ↓
Simulator or quantum device
    ↓
Measurement results
    ↓
Classical analysis

Quantum states cannot be freely copied like ordinary values, and measurement affects the state. Accordingly, linear types, effect systems, and circuit-based languages are being researched.

Quantum programming is currently used not only with actual devices but also in simulators, education, and algorithm research. Support from hardware SDKs, compilers, and circuit representations is more important than any particular language.

WebAssembly Outside Web Browsers

WebAssembly can be used not only in web browsers but also on servers, in plugins, sandboxes, and portable module-execution environments.

Use cases include the following.

  • Executing native-language modules in browsers
  • Safely isolated plugins
  • Serverless functions
  • A deployment format executable across multiple operating systems
  • Mod systems for games and applications
  • Edge computing
  • A common backend for language runtimes

WebAssembly separates its core computational model from host file and network functions. For an actual program to access operating-system functionality, a host interface such as browser APIs or WASI is required.

Language Embedding and Extension

An application can include an interpreter or virtual machine for another programming language internally. This allows users to extend the program's behavior or automate repetitive tasks.

Major application areas include the following.

  • Game scripting
  • Editor automation
  • Mods
  • Plugins
  • Configuration and rules
  • Simulation scenarios
  • Data-processing pipelines

Lua, Python, JavaScript, and custom domain-specific languages may be used as embedded languages. The host program can expose only safe APIs to scripts and restrict execution time, memory, file access, and network access.

Language embedding increases flexibility, but object lifetimes in the host, error propagation, debugging, version compatibility, and isolation of untrusted code must be addressed.

Language-Selection Criteria

Choosing a language for a particular field cannot be based solely on popularity or execution speed.

Major selection criteria include the following.

  • Whether the target operating systems and hardware are supported
  • Whether required libraries and frameworks exist
  • Whether performance and latency requirements are met
  • Whether memory and power limits are satisfied
  • Whether safety and security requirements are met
  • Whether the language can connect to existing code and data
  • Whether developers and maintainers can be secured
  • Whether build, testing, and debugging tools are sufficient
  • Whether long-term support and compatibility policies exist
  • Whether deployment and updates are easy
  • Whether licensing, standards, and certification requirements are satisfied
  • Whether the language fits the project scale and development period

Language selection does not need to force one language on every part of a project. Components with different requirements can be separated and connected through stable interfaces, allowing a more suitable language to be used for each area.

However, increasing the number of languages also increases the following costs.

  • Build systems and toolchains become more complex.
  • Data must be converted at language boundaries.
  • Different runtimes and memory-management models must be coordinated.
  • Debugging and profiling may become more difficult.
  • Developers must understand multiple ecosystems.
  • ABI and version compatibility must be managed.

Therefore, a multilingual architecture is appropriate when component boundaries are clear and the benefits gained by adding a language exceed the costs of interoperability.

Relationship Between Fields and Languages

The relationship between programming languages and their fields of application is not fixed. When new execution environments, libraries, and compilers emerge, existing languages can expand into new fields. Kotlin expanded from a JVM language into Android and multiplatform development, while Swift is expanding beyond Apple applications into server and embedded fields. Apple's official Swift materials likewise present servers and embedded firmware, in addition to mobile applications, as application areas of the language.

Conversely, a language widely used in a particular field was not necessarily selected because it was technically the best language in every respect. Existing code and libraries, educational materials, development tools, support from hardware vendors, and the labor market have a strong influence. Long-verified code, standards, and interoperability can have greater value than new syntax and features.

The use of programming languages has developed not toward one language replacing every field, but toward cooperation among languages at different levels of abstraction. System- and performance-oriented foundation layers, managed application layers, automation scripts, data-query languages, and domain-specific languages for GPUs are used together within one system.

Therefore, understanding the fields of application of programming languages is not limited to listing what can be built with a particular language. It means identifying the computational model, safety, performance, execution environment, and ecosystem required by each field and selecting languages, runtimes, libraries, and tools according to the purpose of the overall system.

Comparison of Programming Languages

Comparing programming languages is not a matter of determining whether one language is generally superior to another. Each language is designed for different problems, execution environments, safety requirements, development methods, and existing ecosystems, so the relative merits of all languages cannot be determined using one identical criterion. A language suitable for an operating-system kernel is not necessarily the most suitable language for data analysis, and a language that allows short automation programs to be written quickly does not necessarily provide the same advantages in the core code of a real-time renderer.

The actual characteristics of a language are not determined by syntax alone. The language specification, type system, compiler or interpreter, runtime, garbage collector, standard library, package ecosystem, development tools, and target platforms all work together. Even for the same language, performance, memory usage, startup time, and deployment methods may differ according to the implementation, compiler options, libraries, and execution environment.

Programming languages must therefore be compared by separating multiple criteria such as the following.

Comparison criterionMain question
Abstraction and controlTo what extent can hardware and memory be controlled directly?
Execution modelDoes the language use native code, a virtual machine, or an interpreter?
Type systemWhen are errors checked, and how are types expressed?
Memory managementWho manages the lifetimes of objects and resources?
SafetyWhat kinds of errors are prevented by the language and implementation?
PerformanceTo what extent can throughput, latency, and memory usage be controlled?
ConcurrencyHow are multiple tasks and shared state expressed?
PortabilityCan programs run on multiple operating systems and architectures?
InteroperabilityHow easily can existing libraries and other languages be used?
Development productivityHow convenient are writing, modification, testing, and error detection?
Tools and ecosystemAre sufficient compilers, debuggers, packages, and frameworks available?
Stability and compatibilityDoes the language and platform preserve existing code over long periods?

Limitations of Comparison

When comparing programming languages, the language must be distinguished from its implementation. Python is a language, while CPython is its representative implementation. Java is a language, while the JVM is an execution environment that can be used by multiple languages, including Java. C# is a language and is generally used together with .NET, but the C# language specification does not require every conforming implementation to be built on the CLI or .NET. Python's official language reference likewise describes the core syntax and semantics of the language without fixing every implementation detail to CPython.

The distinction between a “compiled language” and an “interpreted language” should also not be treated as an absolute property of the language itself. Java generally translates source code into machine-independent bytecode and executes it on the JVM, while the JVM may interpret or JIT-compile that code. Python implementations may also use different methods, including internal bytecode, virtual machines, and JIT compilation. The Java Language Specification describes machine-independent bytecode as a common compilation result, but this does not mean that the Java language is restricted to one specific execution strategy.

Performance comparisons likewise cannot be concluded from the name of a language alone. Even within the same language, results differ according to algorithms, data structures, compilers, optimization options, runtime warm-up state, garbage-collector settings, CPU architecture, and memory architecture. Startup time for a short command-line program, maximum throughput for a long-running server, and worst-case latency for a real-time audio system are different performance criteria.

Language comparison should therefore examine not merely “what is fastest,” but “at which layer each cost is incurred and how predictably and precisely that cost can be controlled.”

Level of Abstraction and Hardware Control

Programming languages differ in the extent to which they expose the concrete structure of hardware.

C (programming language) can directly handle pointers, memory addresses, integer representations, structure layouts, and manual memory management. The C standard defines the representation and meaning of programs while aiming for portability and efficient execution across various computer systems. However, the exact sizes and alignment of data types and the results of some operations may vary depending on the implementation and target environment.

C++ provides low-level control similar to C while adding high-level abstractions such as classes, templates, generic programming, value semantics, and scope-based resource management. One important principle in C++ design is the zero-overhead abstraction principle: programmers should not pay for features they do not use, and abstractions that are used should not impose unnecessary costs compared with manually written low-level code when implemented appropriately.

Rust can also be used to write systems programs requiring pointers, memory-layout control, native ABIs, and direct resource management, but safe code restricts memory usage through ownership, borrowing, and lifetime rules. Rust ownership is a model in which the compiler checks memory-management rules without using a garbage collector during execution.

Go (programming language) is a general-purpose language designed with systems programming in mind, but it delegates more details of memory and object placement to the runtime than C and Rust. The Go specification describes Go as a language providing static typing, garbage collection, explicit concurrency support, and package-based program organization.

Languages such as Java, C#, Python, and JavaScript generally do not directly manipulate pointer arithmetic or object memory layout, but instead use the object model of a virtual machine or runtime. These abstractions can improve memory safety and portability, but make it relatively difficult to directly control costs such as object layout, collection timing, and dynamic invocation.

The level of abstraction cannot be evaluated merely as high or low. Greater hardware control permits specialized optimizations and device access, but requires programmers to manage platform-specific differences and memory errors directly. Conversely, allowing the runtime to manage more details makes it easier to focus on application logic, but can become a limitation in fields where real-time response and memory layout are important.

Execution Models

The common execution models of representative programming languages can be compared as follows.

LanguageCommon execution modelMain characteristics
C (programming language)AOT native compilationSmall runtime, direct ABI and system access
C++AOT native compilationHigh-level abstractions and target-specific optimization
RustAOT native compilationNative code generation after ownership checking
Go (programming language)AOT native compilationIncludes a garbage collector and language runtime
JavaJVM bytecode with JIT or AOTVirtual machine and managed execution environment
C#Common intermediate language with .NET JIT or AOTCLR and managed runtime
PythonBytecode interpretation, JIT, or native extensions depending on implementationDynamic execution and interactive development
JavaScriptInterpreter and JIT in browser or server enginesClose integration with the host environment
TypeScriptTransformed into JavaScript and executed in a JavaScript environmentType information is generally removed before execution
KotlinJVM, JavaScript, native code, WebAssembly, and other targetsTarget-specific backends and interoperability
SwiftNative compilationValue types and automatic memory management
FortranAOT native compilationNumerical and array-oriented optimization and parallel features

This table represents common implementation methods and does not restrict every possible method by which each language can be implemented. C and C++ can be compiled into WebAssembly or other intermediate representations, while Java and .NET programs can also be distributed using AOT methods. Computationally intensive portions of Python code may be delegated to C, C++, Fortran, or GPU libraries.

ECMAScript, the standard of JavaScript, defines the computational and object-manipulation rules of the language, but leaves external input and output to host environments such as browsers and server runtimes. The actual range of JavaScript functionality is therefore the result of combining the ECMAScript language with browser Web APIs or Node.js APIs.

TypeScript does not replace the JavaScript execution environment. TypeScript statically analyzes the forms and behavior of JavaScript values, removes or transforms type annotations and some syntax, and generates JavaScript. The TypeScript type system centers on structural typing in order to model the ways objects are used in JavaScript.

Static and Dynamic Typing

Statically typed languages primarily check type relationships among variables, expressions, and function calls before execution. C, C++, Rust, Go, Java, C#, Kotlin, Swift, and Fortran belong to this category. The Java specification defines Java as a statically typed language in which the type of every variable and expression is known at compile time.

In dynamically typed languages, values have types during execution, and the actual type of a value is checked when an operation is performed. Python and JavaScript are representative examples. In Python's data model, all data is represented as objects or relationships between objects, and each object has an identity, a type, and a value.

The difference between static and dynamic typing does not simply depend on whether type names are written in variable declarations. Rust, Kotlin, Swift, and C# can infer the types of local variables while still performing static type checking. Conversely, Python and JavaScript also perform type checks and runtime object checks.

Major advantages of static typing include the following.

  • Some errors can be discovered before execution.
  • Function and module contracts can be expressed through types.
  • IDEs can more easily provide code navigation, completion, and refactoring.
  • Compilers can use information about values and call relationships for optimization.
  • The effects of large-scale code changes are easier to inspect.

Major advantages of dynamic typing include the following.

  • Data and objects whose forms change during execution can be handled naturally.
  • Small programs and experimental code can be written quickly.
  • Abstractions can be built without complex generic or type declarations.
  • Reflection, metaprogramming, and dynamic loading can be used directly.

Logical errors that cannot be represented by a type system remain even in statically typed languages, while dynamically typed languages can achieve high reliability through sufficient testing and static analysis. The timing of type checking is only one of several factors that determine software quality.

Nominal and Structural Typing

Classes and interfaces in Java, C#, Kotlin, and Swift primarily follow nominal type systems based on declared names and explicit inheritance or implementation relationships. Even when two classes have identical fields and methods, they are generally not treated as the same type unless an explicit relationship exists.

TypeScript primarily uses a structural type system that compares the shapes of the members contained by objects. Even without separately declaring the implementation of an interface, an object may be compatible with the type when it contains all required fields and methods.

Go interfaces also have a structural character: a type satisfies an interface when it provides the required method set without an explicit implements declaration. However, Go's overall type system and TypeScript's structural object types are not based on identical rules and should not be treated as exactly the same system.

Nominal typing is advantageous for clearly distinguishing domain concepts and API boundaries. Structural typing can reduce coupling by allowing existing objects to be used with a required interface without modification. However, it is also necessary to consider the possibility that values with accidentally identical structures may become compatible in unintended locations.

Generics and Generic Programming

C++ and Rust generics can generally generate code specialized for concrete types during compilation. This enables type-specific optimization and static dispatch, but using many combinations of types can increase compilation time and code size.

Java generics primarily use type erasure so that multiple type instances share common bytecode. .NET generics preserve type information at runtime, and code-sharing and specialization strategies may differ between value types and reference types. The C# type system unifies value types and reference types so that every value can ultimately be treated as an object.

Go type parameters use interface-like constraints to determine permitted types and operations. Rust uses trait bounds, while C++ uses templates and concepts. Haskell-family languages express generalization primarily through parametric polymorphism and type classes.

Even when generic syntax appears similar, implementation methods, type constraints, and the presence of runtime type information differ, producing different library-design and performance characteristics.

Memory-Management Methods

Memory management is one of the most important criteria when comparing programming languages.

Manual Memory Management

In C, the programmer directly determines when memory is allocated and released. This permits detailed control of resource usage, but the programmer must directly prevent memory leaks, use after free, double free, and out-of-bounds buffer access.

C++ permits manual allocation while also providing RAII, smart pointers, and containers that connect resource lifetimes to object scopes. Appropriate C++ code can reduce direct use of new and delete and automatically clean up resources when objects leave scope. However, raw pointers and manual resource-management features remain available, so overall safety depends on code design and rules.

Ownership-Based Management

Rust assigns an owner to each value and cleans up the resource when the owner leaves scope. References borrow values without owning them, while the compiler checks relationships between mutable and immutable references and their lifetimes. This structure aims to provide memory safety without a garbage collector.

An advantage of Rust is that many memory errors and potential data races can be restricted at compile time. However, ownership and lifetimes must be considered explicitly when expressing complex sharing structures, self-referential structures, and asynchronous lifetimes, and some low-level tasks may require unsafe code.

Garbage Collection

Major implementations of Go, Java, C#, Python, and JavaScript provide automatic memory management. Because the runtime finds and reclaims objects that the program can no longer reach, ordinary objects do not need to be released manually.

The .NET garbage collector allocates objects in a managed heap and reclaims objects that cannot be reached from roots such as stacks, static fields, and CPU registers. It may also compact objects to reduce memory fragmentation.

Garbage collection reduces the burden of managing object lifetimes, but can introduce collection timing, pauses, additional memory usage, and runtime complexity. Modern collectors reduce these costs using generational collection, concurrent collection, and incremental processing, but environments with strict worst-case latency requirements require separate consideration.

Reference Counting

Swift manages the lifetimes of class instances through automatic reference counting while extensively using value types such as structures and enumerations. Apple's Swift documentation explains that common types such as arrays and dictionaries are value types and that Swift provides initialization, bounds checking, and memory safety as major parts of the language design.

Reference counting makes lifetime timing relatively easy to understand because cleanup occurs when the final strong reference disappears, but strong reference cycles must be prevented separately, and reference increment and decrement operations incur costs.

Memory Safety

Memory safety is the property that a program accesses only valid memory with the correct type and within the correct lifetime.

C and C++ permit direct memory access and reinterpretation, providing broad control, but safe subsets, coding rules, static analysis, runtime checks, and library design are important. Invalid pointers and range errors can result in undefined behavior.

Rust checks ownership and borrowing rules in safe code, but provides unsafe for foreign functions, raw pointers, and certain systems operations. unsafe does not disable every compiler check; it allows some additional operations, such as dereferencing raw pointers, and places responsibility for their safety conditions on the programmer.

Managed languages such as Java, C#, and Go restrict arbitrary pointer operations in ordinary code and check array bounds and object types. C# allows most code to be written as verifiably safe code, but permits pointers and direct memory access in explicit unsafe regions.

Swift also provides safe default behavior, but offers the UnsafePointer family for C interoperability and specialized optimization. When these pointers are used, the developer becomes responsible for automatic memory-management and some type and lifetime guarantees.

Rather than absolutely dividing languages into “safe” and “unsafe,” it is more appropriate to compare which errors are blocked in ordinary code, what bypass features exist, and how clearly safety boundaries can be constructed.

Value Semantics and Reference Semantics

Languages also differ in their fundamental methods of copying and sharing values.

Structures and ordinary objects in C and C++ can generally be copied by value. Pointers or references can be used to share the same object. In C++, copy and move constructors and assignment operators can define how values are copied and ownership is transferred.

Rust distinguishes non-copy types that move by default from types that can be copied explicitly. When a value owning a heap resource is assigned or passed to a function, its ownership may move, and use through the previous name is restricted.

In Java, Python, and JavaScript, object variables generally contain references to objects. Assignment copies a reference rather than the entire object. Some values, such as integers and strings, are immutable or follow separate value rules, but changes to user-defined objects may be observed through multiple references.

C# explicitly distinguishes value types from reference types. Structures and enumerations are value types, and their values are copied during assignment and argument passing. Classes are reference types.

Swift treats structures and enumerations as value types and class instances as reference types. Standard collections also provide value semantics while internally using copy-on-write optimization.

Value semantics can reduce unexpected propagation of object changes and be advantageous for parallel processing. Reference semantics make it possible for multiple components to share the same state without copying large objects, but aliasing and synchronization relationships must be managed.

Object-Oriented Models

Java and C# support object-oriented programming primarily through classes, interfaces, inheritance, virtual methods, and nominal types. In addition to classes, C# provides value-type structures and record constructs and handles all types within a unified type system.

C++ provides classes, multiple inheritance, and virtual functions together with value semantics and template-based static polymorphism. Object-oriented programming is one of several paradigms in C++, and not every abstraction must use inheritance and virtual invocation.

Rust does not provide traditional class inheritance. Methods are defined on structures and enumerations, while traits abstract common behavior. Dynamic trait objects can also be used, but generics and static dispatch are commonly used together.

Go likewise does not provide class inheritance and instead uses structures, methods, interfaces, and composition. A type can satisfy an interface by providing the required method set without explicitly declaring implementation.

JavaScript uses a prototype-based object model. Although it provides class syntax, object inheritance relationships are based on prototype links. The ECMAScript specification defines objects as collections of properties and places object, function, and prototype behavior at the center of the language's semantics.

Which object model is superior depends on the abstraction being used. Classes and interfaces may be appropriate for stable nominal API contracts, while value types and sum types may be appropriate for data-oriented models. Prototypes and dynamic properties may be advantageous in environments where dynamic object extension and metaprogramming are important.

Error Handling

Languages also differ in how they propagate errors.

C generally uses return values, error codes, and global error states. Error handling is explicit, but errors may be ignored if callers do not check return values.

C++, Java, C#, Python, JavaScript, Kotlin, and Swift provide exception-handling structures. Exceptions can automatically propagate upward through multiple call levels, but it may be difficult to determine which failures can occur from a function's ordinary return type alone.

Rust primarily represents recoverable errors using sum types such as Result<T, E> and may use panic for unrecoverable errors. Go centers on returning errors as ordinary values. Both approaches make the possibility of error explicit in calling code, but require language syntax and idiomatic patterns to reduce repetitive propagation code.

Swift can use throws, do, try, catch, optional values, and result types, while Kotlin interoperates with Java but does not enforce checked exceptions in its language type system.

Error handling is not merely a syntactic preference. It affects library APIs, resource cleanup, asynchronous execution, transaction boundaries, and program-recovery strategies.

Null Safety

In languages such as Java and early C#, most reference types include null, and programmers must separately check whether a value refers to an actual object. Modern C# can distinguish string and string? at the source level through nullable reference-type analysis, but the two annotations do not create separate runtime types and are used for compiler static analysis.

Kotlin syntactically distinguishes non-null and nullable types. In principle, null cannot be assigned to String and is permitted only for String?. However, when interoperating with Java, Java declarations may not contain sufficient nullability information, so platform types and external annotations must also be handled.

Rust does not include null in ordinary references and represents the absence of a value using Option<T>. Swift likewise uses optional types to distinguish the possibility that a value is absent.

In Python and JavaScript, values such as None, null, and undefined are used during dynamic execution, while static analyzers and TypeScript can check some possibilities.

Null-safety features can greatly reduce the scope of null-reference errors, but errors can still occur through external code, initialization processes, forced assertions, and unsafe interfaces.

Concurrency and Parallelism

Concurrency support is influenced heavily not only by language syntax but also by runtimes and standard libraries.

C and C++ use operating-system threads, atomic operations, locks, and memory models. Low-level control is possible, but correctness of shared state, lifetimes, and synchronization must be managed directly.

Rust also supports threads, locks, atomic operations, and asynchronous execution. Ownership and type relationships such as Send and Sync can statically restrict whether particular values can be moved or shared between threads. Shared-memory concurrency still requires lock and atomicity design, but some data races can be blocked during type checking.

Go provides goroutines and channels as core concurrency models of the language and runtime. Goroutines are lighter-weight task units than operating-system threads, while channels are used for value transfer and synchronization between goroutines. However, Go can also use shared memory and locks, so channels are not its only concurrency method.

Java and C# provide thread pools, futures, tasks, async and await, concurrent collections, and managed runtimes. The long-established server ecosystems of the JVM and .NET provide tools and libraries for constructing large-scale concurrent services.

Kotlin coroutines combine the language's suspending-function capability with a library-based execution model. Execution schedulers, channels, flows, and similar elements are provided primarily by the kotlinx.coroutines library. Coroutine contexts include elements such as jobs and dispatchers.

JavaScript generally handles asynchronous input/output using an event loop, promises, and async and await. Browser and server runtimes can provide actual parallel or asynchronous work through task queues, Web Workers, and native input/output systems.

Concise concurrency features do not automatically solve race conditions, deadlocks, cancellation, or partial failures. The design of shared state, task lifetimes, error propagation, and backpressure is more important.

Performance and Predictability

Performance consists of several factors, including throughput, latency, startup time, memory usage, binary size, and power consumption.

C, C++, and Rust generally use native machine code and small execution boundaries, allowing detailed control over memory layout, allocation, and calling methods. This is advantageous in fields requiring low-level optimization and predictable latency. However, high performance is not automatically guaranteed, and inappropriate data structures and memory access patterns can cause substantial performance degradation in any language.

Go generates native code but includes a garbage collector and goroutine scheduler. It can simplify the organization of server and network programs, but collection and scheduling costs must be considered.

Java and C# JIT compilers can optimize code based on actual execution information. Long-running programs can benefit from optimizations such as dynamic inlining and type specialization, but program startup, warm-up stages, code caches, and garbage collection affect performance.

Dynamic languages such as Python and JavaScript may incur costs from handling each value's type and operations during execution. JavaScript JIT engines can specialize repeated types and object shapes, but deoptimization is required when assumptions become invalid. Python commonly reduces interpreter-loop costs by performing bulk operations in libraries written in C, C++, and Fortran.

Fortran evolved around arrays, numerical operations, and scientific computing and supports not only imperative and procedural programming but also array-oriented, object-oriented, and functional approaches. Fortran language structures and compilers remain important options for numerical and parallel programs.

Performance comparisons should measure actual implementations and workloads rather than relying on the general image of a language. String processing, database waiting, numerical arrays, memory allocation, and network input/output require different language and runtime characteristics.

Compilation Time and Startup Time

Compilation time for C and C++ may vary greatly according to header inclusion, template instantiation, and optimization level. Complex C++ templates and whole-program optimization can require particularly high compilation costs.

Rust compilation time can also become an important consideration in large projects because of generic monomorphization, ownership and type checking, and LLVM optimization.

Go adopted fast compilation and simple dependency relationships as major design directions and manages dependencies through its package structure.

Java and C# can generate bytecode or intermediate language from source relatively quickly, but additional JIT compilation may occur during execution. Startup characteristics can be adjusted using AOT compilation and prepared runtime images.

Python and JavaScript allow programs to execute immediately without a full native build, which is advantageous for development and short scripts. However, module loading, runtime initialization, and JIT warm-up may be included in startup time.

Startup time is important for short-lived command-line programs and serverless functions, while steady-state throughput may be more important than initial warm-up costs for long-running servers.

Deployment Methods

Programs written in C, C++, Rust, and Go are generally distributed as native executables for the target operating system and CPU. Static linking can reduce external dependencies, but file size, licensing, and system-library policies must be considered. Dynamic linking allows libraries to be shared, but depends on the ABI and library versions of the target system.

Java programs require a JVM and class or JAR files. The runtime may be packaged with the program or a JVM installed in the target environment may be used. C# and .NET can also use several approaches, including framework-dependent deployment, self-contained deployment, and native AOT.

Python programs require an interpreter and packages. They can be bundled into standalone packages, but native extensions and operating-system-specific dependencies may require management of deployment size and compatibility.

For JavaScript frontends, the browser generally serves as the runtime, while a bundler organizes modules into distribution files. Server-side JavaScript requires a runtime such as Node.js and its packages.

Swift and Kotlin have different deployment models depending on the target platform. Kotlin/JVM uses the JVM, while Kotlin/Native can generate native binaries. Swift can produce native executables for Apple platforms, Linux, Windows, and other supported systems.

Platform Portability

C and C++ have implementations for nearly every major operating system and processor and provide convenient access to hardware and operating-system APIs. However, programs may need to handle differences in platform-specific type sizes, ABIs, file systems, and system APIs directly.

Rust and Go also support major desktop, server, and some embedded architectures. The level of support depends not only on the language specification but also on compiler backends, standard libraries, and the maturity of platform-specific toolchains.

Java was designed to execute the same class format across multiple systems using the JVM as a common execution layer. However, platform differences reappear when native libraries, graphics, and operating-system integration are used.

C# and .NET support multiple operating systems, but particular UI frameworks and operating-system APIs may remain platform-specific.

JavaScript has a broad common execution environment in the form of browsers, but differences in browser feature support and host APIs must be considered.

Even when languages such as Kotlin and Swift support multiple platforms, the frameworks and libraries available on each platform may differ. A language being multiplatform and an entire application running unchanged on every platform are separate matters.

Interoperability with External Languages

The C ABI is relatively simple and supported by many languages and operating systems, so it is widely used as a common boundary between languages. C++, Rust, Swift, Python, Java, and C# can call C functions or provide interfaces callable from C.

The C++ ABI is more strongly affected by classes, name mangling, exceptions, standard-library types, and compiler versions. Stable external interfaces therefore often use the C ABI and opaque handles.

Kotlin treats Java interoperability as a major design goal, allowing most Java code to be used directly from Kotlin and exposing Kotlin code to Java.

C# shares a common type system and intermediate language with other .NET languages and can use P/Invoke and native-interoperability features.

Python can connect native libraries through the C API and extension modules. This forms the basis for using Python as a high-level control layer in scientific computing and machine learning while delegating computationally intensive parts to native libraries.

Swift provides interoperability with Objective-C and C, and its range of C++ interoperability is also expanding. However, memory and lifetime rules must be checked directly when unsafe external interfaces are used.

Standardization and Language Stability

C and C++ specifications are managed through ISO/IEC standards. WG14 is responsible for current C standardization, while WG21 is responsible for C++ standardization. International standards provide a foundation on which multiple independent implementations can follow common language rules.

Java provides separate language and JVM specifications, clearly distinguishing the source language from the virtual machine.

ECMAScript evolves around TC39's living specification, with completed proposals incorporated into the latest specification.

Python, Rust, Go, Kotlin, and Swift evolve through their respective public proposal processes and project governance. Even without international standards, practical compatibility can be provided through clear reference documentation, stability policies, and the quality of major implementations.

A standardized language does not mean that every implementation always supports every latest feature at the same time. Conversely, a language managed by a single project may distribute new features rapidly, but dependence on the implementation and governance may be greater.

Development Productivity

Development productivity cannot be measured solely by lines of code. It is affected by how early a language discovers errors, how fast builds and tests run, how easily libraries can be used, and how understandable its error messages and tools are.

Python is suitable for automation and experimental development through dynamic objects, concise syntax, and interactive execution. In large codebases, however, type hints, static analysis, testing, and module boundaries become important.

Java and C# support management of large applications through explicit types and object models, broad standard libraries, and mature IDEs. Their code may be relatively verbose, but it provides substantial information for refactoring and tool analysis.

Kotlin preserves compatibility with the Java ecosystem while providing null safety, type inference, extension functions, and concise data representation. Because it uses the same platform as Java, existing libraries can be used, but differences in null information and API conventions between Java and Kotlin must be considered.

TypeScript preserves the existing JavaScript ecosystem while adding static typing and editor analysis. Its type system is highly flexible in order to model dynamic patterns expressible in JavaScript, but some types are removed without runtime checking, so external data must be validated separately.

Rust allows the compiler to discover many ownership, lifetime, and trait-constraint errors before execution, but initial design and learning costs may be higher. In long-lived systems code, the cost of static checking can lead to fewer defects and more stable refactoring.

C and C++ provide excellent access to hardware and existing libraries, but require direct management of a broad range of memory, ABI, build-configuration, and platform differences. Because C++ provides many abstractions and legacy features, team-level language subsets and coding rules may be important.

Comparison of Representative Language Groups

C, C++, and Rust

The three languages can be used for native systems programming and low-level control, but differ in safety and abstraction models.

CriterionC (programming language)C++Rust
Core abstractionsFunctions, structures, pointersClasses, value types, templates, RAIIStructures, enumerations, traits, ownership
Memory managementPrimarily manualRAII combined with manual managementOwnership and borrowing
Garbage collectionNoneNoneNone
GenericsPrimarily macros and void*Templates and conceptsGenerics and traits
InheritanceNoneClass inheritance supportedNo class inheritance
Unsafe accessOrdinary language functionalityOrdinary language functionalitySeparated through unsafe boundaries
ABIC ABI widely usedStrong implementation and platform dependenceExternal boundaries commonly use the C ABI
Main advantagesSimple execution model and broad supportHigh expressiveness and existing ecosystemStatic memory and concurrency safety
Main burdensDirect memory-safety managementFeature complexity and unsafe legacy structuresOwnership model and compilation cost

C is suitable for boundary code where a small runtime and simple ABI are important. C++ has strengths in high-performance applications, large-scale abstractions, and extensive existing libraries. Rust is advantageous when strengthening memory safety in new systems components, but directly replacing existing C++ ecosystems and complex object models incurs separate costs.

Go, Java, and C#

These three languages are often compared for server and enterprise applications and network services, but their execution models and language philosophies differ.

CriterionGo (programming language)JavaC#
ExecutionNative code with Go runtimeJVM.NET CLR
MemoryGarbage collectionGarbage collectionGarbage collection
Object modelStructures, methods, structural interfacesClasses and nominal interfacesClasses, structures, and nominal interfaces
ConcurrencyGoroutines and channelsThreads, futures, virtual threads, and othersTasks and async·await
GenericsType parameters and constraintsErasure-based genericsGenerics with runtime type information
DeploymentNative executableJVM and bytecodeRuntime-dependent, self-contained, or AOT
Design tendencySmall language and integrated toolsStable JVM ecosystemLanguage features and .NET integration

Go limits language features comparatively and emphasizes simplicity in concurrent network programs, builds, and deployment. Java's strengths lie in the JVM and its long-established server and enterprise ecosystem. C# is tightly integrated with .NET and provides broad language features including value types, LINQ, asynchronous functions, and pattern matching.

Java and Kotlin

Kotlin was designed not to replace Java with a completely separate ecosystem, but to modernize language expression while using the JVM and Java libraries.

CriterionJavaKotlin
Main execution environmentJVMJVM, JavaScript, Native, WebAssembly
Null handlingnull permitted in traditional referencesNullable types are distinguished
Code expressionExplicit and conservativeType inference and concise syntax
AsynchronyThreads, futures, virtual threadsCoroutines and library-based execution
InteroperabilityFoundation of the JVM ecosystemDirect interoperability with Java
Evolution modelJLS and JEPKotlin specification, KEEP, and project processes

When an existing Java organization, libraries, and long-term compatibility are important, Java itself may be the simpler choice. Kotlin can be advantageous when stronger null analysis, concise APIs, and coroutines are desired within the same JVM ecosystem. When the two languages are used together, differences in Java null information, checked exceptions, static methods, and property representations must be considered.

JavaScript and TypeScript

TypeScript is not a separate runtime language competing with JavaScript, but a language layer that statically analyzes JavaScript programs and transforms them into JavaScript.

CriterionJavaScriptTypeScript
Type checkingPrimarily during executionAdds pre-compilation static analysis
ExecutionBrowser and server enginesExecutes transformed JavaScript
Type systemDynamic object typesStructural static types
External dataProcessed directly during executionRuntime validation required separately from type declarations
Existing codeDirectly executableCan gradually include JavaScript
Main advantageFundamental language of every web environmentSupports navigation and modification of large codebases

JavaScript alone may be sufficient for small scripts and dynamic code. In large applications maintained by multiple developers, TypeScript can assist API contracts and refactoring. However, TypeScript types are generally removed during execution and therefore do not automatically guarantee that JSON received over a network actually satisfies a declared type.

Python and Statically Compiled Languages

Python is often simply classified as having lower execution performance than statically compiled native languages, but division of roles is important in actual systems.

Python can be used for program organization, automation, interactive analysis, and model definition, while compute-intensive operations are delegated to C, C++, Fortran, Rust, or GPU libraries. In this structure, passing large units of work to native libraries is more important than the iteration speed of Python code itself.

Statically compiled native languages are advantageous for repeated small operations, memory layout, and latency control. Python may be advantageous for development and experimentation speed because of concise expression, dynamic objects, and extensive libraries.

Rather than choosing only Python or C++, a mixed structure in which Python handles the high-level workflow and C++ handles core computation may be appropriate.

Swift and Kotlin

Swift and Kotlin are frequently compared in the Apple and Android ecosystems respectively, but the execution scope of the languages themselves is not limited to mobile platforms.

Swift centers on native compilation, value types, automatic reference counting, memory safety, and integration with Apple frameworks. Kotlin provides JVM interoperability, garbage collection, null safety, coroutines, and multiplatform backends.

Swift is the natural choice when native UI and system APIs on Apple platforms are central, while Kotlin is natural in Android and JVM ecosystems. When business logic must be shared between the two platforms, structures such as Kotlin Multiplatform or a common core written in C++ or Rust may be considered.

Common Comparison Errors

Treating Language Speed and Implementation Speed as Identical

A language specification defines program meaning but does not determine every detail of generated machine code and optimization. The same language may exhibit different performance across implementations.

Assuming Shorter Code Always Means Greater Productivity

Short code may improve writing speed, but hidden behavior, dynamic errors, and maintenance costs may increase. Conversely, explicit types and error handling make code longer but can assist large-scale changes.

Assuming Static Typing Eliminates Errors

Static typing blocks only errors represented by the type system. Incorrect algorithms, security policies, database-transaction errors, and external-input problems must be handled separately.

Assuming Garbage Collection Is Always Slow

A garbage collector can process allocations quickly and improve locality by compacting objects. However, collection timing and memory headroom are important costs. Results differ according to workload and collector design.

Assuming Native Languages Are Always Faster Than Managed Languages

A JIT compiler can use actual execution information for dynamic optimization. Conversely, a native compiler can perform whole-program analysis and target-specific optimization before execution. Which is advantageous depends on the program, execution duration, and implementation.

Assuming Modern Languages Always Replace Older Languages

Existing code, libraries, ABIs, personnel, and verification records are major assets. Fortran, COBOL, and C remain in use not merely because of syntax, but because of accumulated systems and ecosystems.

Assuming Support for Multiple Paradigms Is Always Better

More features allow various problems to be expressed, but can increase language and codebase complexity. A small and restricted language can help an entire team use a consistent approach.

How to Select a Language

When choosing a programming language, the project's mandatory conditions must first be separated from conditions that can be changed.

Execution Environment

The target operating systems, CPUs, browsers, mobile platforms, game consoles, and embedded devices must be identified first. Languages and SDKs officially supported by a particular platform may effectively determine the available choices.

Performance Requirements

Not only average throughput but also worst-case latency, startup time, memory limits, binary size, and power usage must be defined concretely. Prototypes should be measured using workloads resembling actual use.

Safety and Reliability

Memory safety, type errors, data races, nullability, error handling, and certification requirements must be reviewed. Not only language features but also coding standards, static analysis, and testing tools should be evaluated.

Existing Ecosystem

It is necessary to determine which languages are supported by required database drivers, graphics APIs, machine-learning frameworks, device SDKs, and enterprise systems. When a core library does not exist, the cost of implementing it directly may exceed the syntactic advantages of the language.

Development Organization

Developer experience, recruitment availability, training costs, code-review methods, and operational systems must be considered. Using a language understood by only a small number of specialists throughout a core system can create long-term maintenance risks.

Deployment and Operations

It is necessary to review how the program will be installed, updated, and observed. Runtime installation, container size, cold starts, debugging, and profiling tools affect operational costs.

Long-Term Maintenance

The governance, compatibility policies, standardization status, and version-support periods of the language and implementation must be inspected. The ability to update code and dependencies over several years may be more important than short-term development speed.

Practical Decision Table for Comparison

RequirementCharacteristics to examine firstRepresentative language choices
Operating systems and driversNative execution, ABI, memory controlC, C++, Rust
High-performance games and renderingPredictable latency, graphics ecosystemC++, Rust, C#
Network servicesConcurrency, deployment, server ecosystemGo, Java, C#, Rust, Kotlin
Enterprise business systemsCompatibility, managed runtime, frameworksJava, C#, Kotlin
Web frontendBrowser execution and Web APIsJavaScript, TypeScript
Automation and toolsRapid writing, file and process handlingPython, shell, PowerShell
Data analysisInteractive execution, statistics and numerical ecosystemPython, R, Julia
Scientific and numerical computingArray operations, parallel computing, existing codeFortran, C++, Python, Julia
Mobile applicationsPlatform SDKs and UI frameworksKotlin, Swift
Embedded devicesSmall runtime, hardware accessC, C++, Rust, Ada
Safety-critical systemsVerifiability, restricted execution modelAda, C, C++, Rust
Plugins and modsEmbeddability, sandboxing, simple APIsLua, JavaScript, WebAssembly
Rapid prototypesImmediate execution and modificationPython, JavaScript
Large statically typed web codebasesStructural typing and JavaScript ecosystemTypeScript

This table is not an absolute recommendation list. Even within the same field, a different choice may be more suitable depending on existing technology, team experience, platform, and licensing.

Conclusion

Differences between programming languages are not limited to differences in syntactic expression. Development methods, types of errors, and performance characteristics vary according to how values and objects are modeled, when types are checked, who manages memory and resources, and which execution environment runs the program.

C and C++ provide broad control over hardware and existing systems ecosystems. Rust strengthens ownership-based safety in similar areas. Go provides a comparatively small language and runtime-based concurrency. Java and C# center on managed platforms and large application ecosystems. Python and JavaScript provide dynamic execution, rapid development, and broad application environments, while TypeScript adds static analysis to the JavaScript ecosystem. Kotlin and Swift combine modern type and safety features with major platform ecosystems. Fortran has a long-established scientific and engineering ecosystem centered on arrays and numerical computation.

No language is simultaneously superior under every comparison criterion. Strengthening low-level control broadens the area for which programmers are responsible, while strengthening automatic management broadens the area handled by the runtime and implementation. Stronger static checking can prevent some errors in advance but may make the type system and compilation process more complex, while stronger dynamic execution increases flexibility but places greater responsibility on runtime validation and testing.

The purpose of comparing programming languages is therefore not to determine one final winner. It is to understand which costs and constraints each language has chosen in order to solve particular problems and to make the choice most suitable for the project's execution environment, safety requirements, performance requirements, development organization, and existing ecosystem.

Related Documents