最近
llamafile
的作者在实现一个高亮语法器,支持的编程语言包括 Ada、Assembly、BASIC、C、C#、C++、COBOL、CSS、D、FORTH、FORTRAN、Go、Haskell、HTML、Java、JavaScript、Julia、JSON、Kotlin、ld、LISP、Lua、m4、Make、Markdown、MATLAB、Pascal、Perl、PHP、Python、R、Ruby、Rust、Scala、Shell、SQL、Swift、Tcl、TeX、TXT、TypeScript 和 Zig ,这些语言几乎涵盖了 TIOBE 指数上的所有内容:
在实现这些语法高亮的过程中发现了非常多的奇葩语法,每个语言的设计都有自己的独到之处,下面我们一起来学习一下。
C语言
C 语言是一种通用的高级编程语言,由 Dennis Ritchie 在 1972 年为 Unix 操作系统开发。它以低级语言的效率和高级语言的灵活性著称,适用于系统编程、嵌入式开发等领域。C 语言的影响力深远,许多现代编程语言(如 C++、Java、JavaScript 等)都借鉴了其语法和概念。
三字母词(Trigraphs)
C语言一向以简单著称,但它却引入了一些令人大跌眼镜的语法特性。首先是
三字母词(Trigraphs)
,它们是为了解决某些键盘缺少特定字符的问题。通过使用三字符的组合来替代某些符号:
例如,以下代码实际上是有效的C代码:
int main (int argc, char * argv??(??)) ??< printf ("hello world\n" ) ; ??>
尽管在C23标准中三字母词被移除了,但为了兼容旧代码,许多编译器仍然支持这种语法。
通用字符名(Universal Character Names)
C语言还引入了
通用字符名
,允许在代码中使用
\u
或
\U
开头的Unicode字符。例如:
int \uFEB2 = 1 ;
虽然看似有助于支持多语言编程,但实际应用中,这种用法并不常见,且编译器对可用的 Unicode 范围有严格限制。
多行注释与多行字符串
在 C 中,单行注释不能跨多行,但如果在每行末尾添加反斜杠
\\
,则可以实现多行单行注释:
// 这是一个\ 多行注释
类似地,字符串也可以使用反斜杠延续到下一行。
Haskell 的嵌套注释
Haskell是一种函数式编程语言,以其强大的类型系统和纯函数式特性著称。Haskell 由 Haskell Curry 命名,支持高阶函数、惰性计算和不可变数据,是教学、研究和开发高度可靠的软件的重要工具。
很多语言不支持注释的嵌套,例如C语言中的多行注释。然而,
Haskell
允许嵌套注释,这在调试和代码说明中非常便利:
{- 这是一个注释块 {- 嵌套的注释 -} -}
Tcl中的特殊标识符
Tcl(Tool Command Language)是一种动态类型的脚本语言,1988 年由 John Osterhout 发明。Tcl 以其简单的语法和强大的扩展能力广受欢迎,常用于嵌入式脚本、快速原型设计和应用程序脚本化。
Tcl
语言的标识符可以包含引号,这意味着以下代码是有效的:
puts a"b
甚至变量名中也可以包含引号,需要通过
$
加花括号的方式引用:
set a"b "Hello, World!" puts ${a"b}
JavaScript的正则表达式陷阱
JavaScript是一种动态、弱类型的脚本语言,主要用于网页开发。由 Netscape 在 1995 年推出,其语法受 C 语言影响,支持面向对象、函数式和命令式编程。JavaScript 是前端开发的必备语言,广泛用于客户端和服务器端脚本开发。
JavaScript中的正则表达式语法有时会让人困惑。例如,以下正则表达式是有效的:
var regex = /[/]/g ;
在字符集
[]
内部的斜杠
/
不需要转义,这与在正则表达式外部的用法不同。
此外,JavaScript还支持一些不可见的Unicode字符作为行终止符,如
LINE SEPARATOR (\u2028)
和
PARAGRAPH SEPARATOR (\u2029)
,这可能导致代码在不同环境下的行为不一致。
Shell的Here Document奇技
Shell脚本语言是 Unix/Linux 系统中的命令解释器和脚本编程语言。常见的 Shell 有 Bash、sh 等。Shell 脚本用于自动化任务、系统管理和批处理工作,是系统管理员和开发者的得力工具。
在Shell脚本中,
Here Document
允许定义多行字符串。通常的用法是:
cat < 这是一个 多行字符串 EOF
但你可以使用一个空的界定符,这会使Here Document在遇到空行时结束:
cat <<'' Helloecho "这将被执行,而不是字符串的一部分"
以上代码中,字符串在第一次空行时结束,后续的
echo
命令将被执行。
字符串插值的奇异用法
许多现代编程语言支持字符串插值,例如TypeScript、Kotlin、Scala等,它们允许在字符串中嵌入变量甚至表达式。
Kotlin的字符串插值
Kotlin是一种现代编程语言,由 JetBrains 在 2011 年发布,旨在改善 Java 语言的不足。Kotlin 兼容 Java,并且具有更简洁的语法、空安全、扩展函数等特点。它被广泛应用于 Android 开发、Web 开发和企业级应用。
val s = "${name.toUpperCase()} , 欢迎您!"
但当嵌套的花括号数量增加时,解析和高亮这些字符串变得复杂。
Swift的自定义分隔符
Swift是 Apple 公司为 macOS、iOS、watchOS 和 tvOS 开发的编程语言,发布于 2014 年。Swift 以其现代语法、安全性和高性能著称,并逐渐取代了 Objective-C 在苹果生态中的地位。
Swift
允许在字符串的起始位置添加任意数量的
#
号,使得字符串中的特殊字符无需转义:
let rawString = #""" 这是一段包含 " 引号和 \ 反斜杠的字符串 """ #
C#的多重引号字符串
C#
是一种面向对象编程语言,由微软在 2000 年发布,作为其.NET 框架的一部分。C# 结合了 C++ 的强大功能和 Java 的简单性,广泛用于 Windows 应用程序、Web 开发、游戏开发(通过 Unity)等领域。
C#解决字符串中包含引号的问题是使用多重引号,字符串的起始和结束引号数量相同:
Console.WriteLine(@"""这是一句话,包含引号""" ); Console.WriteLine(@"""""""多重引号字符串""""""" );
这种方式使得在字符串中包含任意数量的引号变得简单直观。
Lua的多层次字符串
Lua是一种轻量级、多范式的编程语言,1993 年由巴西里约热内卢天主教大学开发。Lua 以其简单、高效、嵌入式设计和灵活的元表机制受到游戏开发和嵌入式系统开发的青睐。
Lua
使用等号
=
来定义不同层次的长字符串或注释,这使得在字符串中包含任意的
[
和
]
字符成为可能:
-- 长字符串 local str = [==[ 这是一个包含特殊符号的字符串 [[ ]] ]==] ]==]-- 多层注释 --[==[ 这是一个注释 --[=[ 嵌套的注释 ]=] ]==]
Assembly的注释与语法
Assembly(汇编语言)是一种低级编程语言,直接与计算机硬件交互。每条汇编指令通常对应一条机器语言指令。汇编语言用于嵌入式系统、驱动程序开发和系统级编程,需要深入了解硬件结构。
汇编语言的语法因汇编器不同而有较大差异,如
NASM
、
AT&T
等。注释符号可能是
;
、
#
、
//
等,甚至在一些老式汇编器中,
/
用于注释。此外,汇编代码通常与预处理器如
cpp
或
m4
结合使用,增加了语法的复杂性。
section .data message db 'Hello, World!', 0xA ; 定义字符串,包含换行符 section .text global _start _start: ; syscall 写操作 mov rax, 1 ; 系统调用编号:write mov rdi, 1 ; 文件描述符:stdout mov rsi, message ; 指向字符串的指针 mov rdx, 13 ; 字符串长度 syscall ; syscall 退出程序 mov rax, 60 ; 系统调用编号:exit xor rdi, rdi ; 退出状态码:0 syscall
Perl的正则表达式与文档块
Assembly(汇编语言)是一种低级编程语言,直接与计算机硬件交互。每条汇编指令通常对应一条机器语言指令。汇编语言用于嵌入式系统、驱动程序开发和系统级编程,需要深入了解硬件结构。
Perl以其强大的正则表达式支持著称,其语法也相当灵活。例如,Perl允许使用不同的定界符来定义正则表达式,以避免转义斜杠:
$string =~ s !hello!Perl!i;
此外,Perl的文档块使用了独特的
=pod
和
=cut
语法,可以在代码中嵌入大段说明性文字。
=pod =head1 NAME MyScript - 演示Perl的文档块 =head1 SYNOPSIS perl MyScript.pl =head1 DESCRIPTION 这是一个示例脚本,展示了Perl的文档块用法。 =cut print "Hello, World!\n" ;
Ruby的语法迷宫
Ruby是一种面向对象的动态编程语言,由 Yukihiro "Matz" Matsumoto 在 1995 年设计。Ruby 以其简洁、优雅的语法和强大的元编程能力著称。Ruby on Rails 是其最著名的 Web 应用开发框架。
Ruby
的语法灵活性极高,甚至让解析器都难以完全理解。例如,反引号既可以用于执行命令,也可以作为方法名:
def ` (cmd) "执行命令:#{cmd} " end
此外,Ruby的字符串、正则表达式、符号等语法经常让人迷惑:
puts "这是#{< 多行字符串 HERE
Ruby允许在字符串插值中嵌入Here Document,这种语法在其他语言中极为罕见。
Ada:单引号的多重用途
Ada是一种结构化、静态类型的编程语言,由 Jean Ichbiah 在 1983 年为美国国防部开发。Ada 以其高可靠性和安全性著称,广泛用于航空、航天和国防领域的高要求系统开发。
字符字面量
像C语言一样,Ada使用单引号来表示字符字面量:
C := 'A';
属性访问
单引号还用于访问类型或对象的属性。例如:
Length := Array'Length;
这里,
Array'Length
表示数组的长度。
泛型和函数调用
更有趣的是,单引号可以用于调用泛型函数或属性函数。例如:
with Ada.Text_IO; procedure Main is S : String := Character'(')')'Image; begin Ada.Text_IO.Put_Line("变量 S 的值是:" & S); end Main;
运行上述代码,会输出:
变量 S 的值是:')'
在这个示例中,我们:
调用了属性函数
:
Image
,将字符转换为字符串表示。
这种单引号的多重用途,使Ada的代码在解析和阅读时需要格外小心。
BASIC:古早语法的奇妙之处
BASIC(Beginner's All-purpose Symbolic Instruction Code)是一种易于学习的编程语言,1964 年由 John Kemeny 和 Thomas Kurtz 开发。BASIC 为初学者设计,通过简洁的语法帮助用户快速编程。虽然早期 BASIC 版本较为简陋,但后来发展出了更加现代和强大的变种,如 Visual BASIC。
代码示例
以下是一个Commodore BASIC的经典代码片段,可以看到它打破了许多我们惯常的语法假设:
10 rem cbm basic v2 示例 20 rem 包含关键字的注释:for, data 30 dim a$(20) 35 rem 缺少空格的节省空间写法: 40 fort=0to15:poke646,t:print"{revers on} ";:next 50 geta$:ifa$=chr$(0):goto40 55 rem 行末的字符串引号可以省略 60 print"{white}":print"再见... 70 end
解析特点
省略空格
:第40行中,
FOR
、变量
T
、赋值符号之间没有空格。这在早期是为了节省内存和存储空间。
模糊变量与关键字
:关键词和变量之间没有明确的分隔符,例如
goto
会被解析成关键字,即使它是标识符的一部分。
行末可省略引号
:字符串末尾的引号可以省略,并不会导致语法错误,例如第60行。
这样的语法设计在今天看来充满了挑战,但它确实展示了早期计算机编程的独特风貌。
Visual BASIC的日期字面量
Visual BASIC 还引入了奇特的日期字面量语法:
Dim v As Variant ' 声明一个Variant变量 v = #1/1/2024# ' 持有一个日期值
同时,VB还支持条件编译指令,这对语法分析提出了更高的要求:
#If DEBUG Then Public Function SomeFunction() As String #Else Public Function SomeFunction() As String #End If
FORTH:简单到极致的语言
FORTH
是一种极其简洁的编程语言,它将所有内容都作为空格分隔的标记处理。虽然简洁,但人类理解起来却非常复杂。这是一种典型的低级语言,直接操作硬件,语法非常紧凑。
代码示例