Unicode in Compilers and Programming Language Design
Modern programming language designers must decide how to handle Unicode in identifiers, string literals, source file encoding, and operator symbols — decisions with deep implications for readability and security. This guide explores how Python, Rust, Swift, and other languages approach Unicode in their language specifications and the trade-offs involved.
Programming languages are themselves text, and the question of which characters are valid in that text — particularly in identifiers like variable and function names — has become one of the more nuanced Unicode design problems in software engineering. Modern languages increasingly allow Unicode identifiers, opening up both internationalization benefits and new security risks.
Identifier Syntax: UAX #31
Unicode Technical Standard #31 (Unicode Identifier and Pattern Syntax) defines recommended character categories for identifiers. The core categories are:
- ID_Start: Characters that may begin an identifier. Includes Unicode letters, letterlike symbols, and ideographs. Excludes digits, combining marks, and punctuation.
- ID_Continue: Characters that may continue (but not start) an identifier. Adds decimal digits, combining marks (diacritics), connector punctuation (underscore), and a few other categories to ID_Start.
A conforming identifier is defined as: one ID_Start character followed by zero or more ID_Continue characters.
UAX #31 also defines Pattern_Syntax and Pattern_White_Space properties for use in programming language tokenizers, ensuring consistent behavior for operators, delimiters, and whitespace across scripts.
Which Languages Allow Unicode Identifiers
Python: Since Python 3.0, identifiers follow UAX #31 with NFC normalization applied. You can write:
# Valid Python 3 — Unicode identifiers
naïve = True
π = 3.14159
数量 = 100
Python applies NFKC normalization to identifiers at compile time, so file (U+FB01, fi-ligature followed by le) and file are treated as the same identifier.
Rust: Allows Unicode identifiers following UAX #31 since Rust 1.0. Raw identifiers (r#keyword) allow even reserved words as identifiers. Rust additionally warns about confusable identifiers via the confusable_idents lint.
Swift: Supports Unicode identifiers broadly, including operators composed of Unicode symbol characters. Swift's operator overloading combined with Unicode symbols can produce expressive mathematical code:
let ∑ = values.reduce(0, +)
let μ = ∑ / Double(values.count)
Java: Has supported Unicode identifiers since Java 1.1, predating UAX #31. Java uses Character.isJavaIdentifierStart() and Character.isJavaIdentifierPart(), which include some characters (currency symbols like $) beyond the UAX #31 definition.
Go: Go allows Unicode letters and digits in identifiers per the Go specification, broadly following UAX #31. Go source files are always UTF-8.
JavaScript (ECMAScript): Identifiers may include Unicode letters as defined by the Unicode "ID_Start" and "ID_Continue" derived properties, or any character via a Unicode escape sequence (\\u{1F600} is technically a valid — if bizarre — identifier character).
C and C++: C99 and C++11 added Universal Character Names (\uXXXX, \UXXXXXXXX) for identifiers, but implementation support in compilers lagged for years. GCC and Clang now support Unicode identifiers directly (not just via UCNs), though portability concerns have kept adoption low in practice.
Security Risks: Confusable Identifiers
Unicode's breadth creates a class of security vulnerability: homograph attacks or confusable identifier attacks. Different Unicode characters can look visually identical or nearly identical depending on the font, making it possible to write code that misleads a human reviewer while behaving differently than it appears.
Some confusable character pairs:
| Character | Codepoint | Looks like |
|---|---|---|
| Latin a | U+0061 | — |
| Cyrillic а | U+0430 | Latin a |
| Greek ο | U+03BF | Latin o |
| Latin 1 | U+006C (l) | digit 1 in some fonts |
A malicious actor could define a function named with Cyrillic characters that looks identical to a function named with Latin characters, then call the malicious version in one place while reviewers assume they are looking at the legitimate version.
Unicode Technical Standard #39 (Unicode Security Mechanisms) addresses this with:
- Confusables data: A machine-readable table of character pairs that are visually similar.
- Identifier restriction levels: Recommended restrictions on mixing scripts within a single identifier (e.g., "do not mix Latin and Cyrillic in the same identifier").
- Whole-script confusables: Pairs of identifiers, each using only one script, that are visually indistinguishable.
Languages handling this: Rust emits a warning for identifiers that are confusable with each other in the same scope. Python added similar warnings. Most other languages do not yet implement confusable detection.
Trojan Source Attacks
The "Trojan Source" vulnerability (CVE-2021-42574, published 2021) is a different Unicode security concern. It exploits bidi override characters — characters that change text direction — to make source code appear different to a human reader than it does to a compiler.
A comment or string literal containing bidi override characters can visually reorder source code so that what appears to be a comment is actually executed code, or what appears to be a condition is reversed. For example:
# A right-to-left override inside a string could make
# closing quote marks appear to end a string earlier
# than they actually do in the file bytes.
Mitigations include: - Compiler warnings: GCC 12+, Clang 13+, and Rust warn on bidi control characters in source files. - Editor display: IDEs should render bidi characters visibly rather than applying their directional effects. - Linters: Security-focused linters can detect bidi overrides in source files.
Source Encoding Detection
The question of what encoding a source file uses predates modern Unicode ubiquity. Different languages handle this differently:
- Python: Requires UTF-8 by default. A
# -*- coding: utf-8 -*-magic comment (or other encoding name) on line 1 or 2 overrides this. - Ruby: Similar magic comment system (
# encoding: utf-8). - Java: Source files are conventionally UTF-8, but the
-encodingjavac flag accepts others. Java source files can also use\\uXXXXescape sequences for non-ASCII characters regardless of file encoding. - Go: Mandates UTF-8 for source files; no override mechanism.
- C/C++: The "source character set" and "execution character set" are distinct concepts in the standard. Compilers typically default to the system encoding; GCC accepts
-finput-charset.
The modern consensus is converging on UTF-8 as the mandatory source encoding, with BOM (byte order mark) discouraged but tolerated.
String Literal Escape Syntax Across Languages
Different languages use different escape sequences for Unicode characters in string literals:
| Language | Short escape | Long escape | Named |
|---|---|---|---|
| Python | \\uXXXX |
\\UXXXXXXXX |
\\N{NAME} |
| JavaScript | \\uXXXX |
\\u{XXXXX} |
No |
| Java | \\uXXXX |
(4-digit only) | No |
| Rust | \\u{XXXXX} |
(variable length) | No |
| Go | \\uXXXX |
\\UXXXXXXXX |
No |
| Swift | \\u{XXXXX} |
(variable length) | No |
| C++ | \\uXXXX |
\\UXXXXXXXX |
No |
Python's \\N{LATIN SMALL LETTER E WITH ACUTE} named escapes are unique and valuable for documentation purposes — they make the intent explicit without requiring the reader to decode a hexadecimal codepoint.
The trend in newer languages (Rust, Swift, modern JavaScript) is toward the \\u{...} syntax with variable-length hex digits and no zero-padding requirement, which is more readable than the fixed-length \\uXXXX form for codepoints requiring more than 4 hex digits.
Advanced Topics의 더 많은 가이드
Sorting text correctly across languages requires the Unicode Collation Algorithm (UCA), which …
ICU (International Components for Unicode) is the reference implementation library for Unicode …
Unicode 16.0 added thousands of new characters, but there are still hundreds …
Unicode normalization must often be applied at scale in search engines, databases, …