查看本文档内容的不同语言版本:
© 2020-2023 统信软件技术有限公司
本仓库维护名为 Unilang 新编程语言,包括相关的文档和参考实现——一个解释器。
解释器的构建和使用参见以下各节。
Unilang 是为适应更有效和灵活开发桌面环境应用的提出的通用目的编程语言项目。
当前桌面应用开发已有许多选项,存在各自的优势和不足:
Qt 代表的 C/C++ 本机应用开发方案是许多 Linux 桌面系统应用的主流方案。
Electron 代表的非本机和动态语言运行时为基础的开发方案是另一类较主流的方案。
PySide 代表的本机和动态语言混合方案能解决以上两类方案的部分问题。
Flutter 代表的移动端解决方案也正在向桌面移植。
从更高层的结构角度来看,不同类型的 GUI 方案也各自存在不同的架构意义上的技术局限性,也极大地限制了真正的通用方案的选择余地:
和传统的保留模式(retained mode) GUI 相比,Dear ImGUI 为代表的立即模式(immediate mode) GUI 缺乏 GUI 作为实体的抽象,不能很好地对应传统的 WIMP 隐喻。
依赖随系统提供的“本机”方案难以克服底层实现的具体功能局限。
WS_EX_LAYERED
在 Windows 8 之前仅在顶层窗口而不在子窗口中受支持。Web 图形客户端(浏览器)为基础的 GUI 具有良好的可移植性和灵活性,但是有其它一些特有问题:
依赖不同其它组件的混合框架存在对应的路径依赖问题。
以 Qt 为代表的本机而非操作系统原生的传统 GUI 框架,鲜有和上述问题能相提并论的全局架构缺陷,但在实现架构和 API 设计上仍然存在诸多问题,开发体验也不尽如人意。
所以,没有任何现有方案能兼顾各种不同的问题而成为没有疑义的桌面开发首选方案。
以上问题中,有相当一部分(性能、部署难度、可移植性)和语言直接相关。考察其中语言部分的问题,我们发现,现有语言不足以兼顾所有这些主要问题,因为:
我们迫切希望有一种新的语言解决以上所有痛点。但是,仅仅提供一种新的语言设计和实现是不够的。新的语言不是自动解决遗留问题的魔法——特别是考虑到市面上并不缺乏“新”的编程语言,却仍未满足需求的现状。
造成这种局面的一个技术理由是,许多设计过于专注具体需求而缺乏考虑语言长期演进的普遍因素,在预期目标领域之外的适用性急剧下降,不够通用,或在平衡通用性和复杂性上失败了。这使得应用领域和预期略有偏差、暴露原有设计的局限性时,用户即便懂得如何改进一个语言,也会在语言二次开发上遇到现实可行的困难,而被迫放弃。
若提出新的选项而不能避免这种状况,只会更加阻碍问题的解决。因此,在能满足需求的基础上,我们希望新的语言能以更深刻的方式真正地实现通用性——通过减少为个别问题领域准备的原生的特设的(ad-hoc) 特性,而以更普遍的基本特性集取而代之的方式。
Programming languages should be designed not by piling feature on top of feature, but by removing the weaknesses and restrictions that make additional features appear necessary.
好吧,但是为什么要用一个新语言呢?
我们知道,编程语言的大多数属性是通过语言规则指定的特性(feature) 提供的。此外,通过故意缺乏一组特定的特性(即*故障特性(misfeature) *),可以保证某些其他所需的特性。
用户想要的是特性,而不是错误特性。事实上,用户所需的具体特性和错误特性集并不是完全固定的。向当前编程语言添加新特性可能很容易。然而,由于(不)兼容性方面的风险,从语言中移除错误特性几乎总是相当困难的(如果不是不可能的话),除非语言从设计上就预期了这种移除。
决定一个特性是否满足我们的需求,需要考虑许多技术问题。其他情形可能需要不同的决策。所以,公共的特性集可能出人意料地小。但是几乎所有工业语言都提供得太多了,而那些多余的特性又不容易按需被用户(而非语言设计者)添加回来。这种情形意味着许多错误特性是不可避免的。
出于实用原因,我们决定不改变当前的语言(这可能会增加语言的复杂性,并给一些用户带来更多的不功能),而是重新发明我们自己的轮子,以解决问题。
我们也更倾向于只使用一种语言来满足需求。否则,我们也可以有几种语言。这些语言自然适用于不同的问题领域,即领域特定语言(DSL) 。
当这些不同领域共享很少的工作时,后者可能运作良好。另一方面,根据我们的经验,对于由一些相关子域组成的稍大的问题域,维护不同的 DSL 是一种痛苦。互操作性方面也存在不必要的成本。即使我们想以不同的工具集专门用于不同的问题域,也应在不同的语言之间复用更多的公共设计和实现。它们在设计上不应完全独立。换句话说,DSL 的替代品最好是通用语言的方言。
一些用户可能会关心这里的“一个”语言的感官体验。的确,一种唯一的、通用的语法(syntax) 很难跨不同的问题领域,因为语法上存在冲突需求:我们可能需要完全不同的视觉风格来实现不同的目的。但实际上,没有任何定律排除在一种语言中同时支持多组具体语法的可能性。
因此,语言的语法不应是问题。如果语法不令人满意,那就改变它。这是由用户而非语言的设计者完成的。这当然会有一些成本,但一旦已有正确的底层语言设计,改动的代价就应比设计和实现完全不同的语言代价更小。
如前所述,要设计一种真正通用的语言,我们不希望将特性堆砌在现有语言之上。
我们没有直接指定丰富的特性集,而是首先提供了基础语言,它只具有足够基本的特性。这些特性可用于以库 API 的形式实现其它特性。这里,语言被派生(derive) 。
基础语言是专门为易于集成而设计的。换句话说,除了公共特征集之外,它还服务于语言派生的问题域,这被大多数编程语言所忽略。与大多数(如果不是所有)主流编程语言相比,它在这方面应该相对容易使用。具体来讲,基础语言在没有其他语言中许多流行的核心语言特性(如类型系统)的情况下仍具有相当可用性。这大大降低了所有语言派生中不成熟设计蕴含的错误特性的风险。
我们理解用户可能需要本机语言设计中未原生提供的开箱即用的特性。这通过允许语言的基本特性更可编程来解决,即大多数特性都在库中。
我们也鼓励用户添加来自当前语言的新功能集,并以库的形式为我们提供贡献。
这种方法类似较小粒度锁改进争用,提高了语言进化过程中潜在的并行性。我们已经确定了一些主要程序语言在开发过程中的具有的较高的通信成本,并试图以这种方式防止一些可预见的问题。这可比任何替代方法更有效。
注释 本节内容可被视为以上两节内容的原理。
注释 本节内容可能涉及不那么周知的理论观点。尽管背景知识都是可查证的,在此有意不给出相关参考文献;这留作对语言设计有兴趣的读者的练习。
这个语言的设计者认为:
constexpr
之间的相同逻辑为何不得不有不同的写法而无法归并?GUI 是一个现实的包含大量非平凡编程主要的问题领域。这个领域同时相当重要和复杂。特别地,它有很多不同的问题子领域,而许多解决方案已经选择了不同 DSL 的混合使用。因此,这对实验我们的方法论而言,是一个很好的例子。
由于问题的内稟复杂性,我们不期望万能的解决方案能够满足所有需求。相对地,我们有不同的计划来改善现状。
在所有计划中,语言的动态性起着重要作用。我们认为,对于部件布局、层次结构、运行时对象内省(inspection) 和热重载等特性,最好是动态的。在静态语言中实现它们的任何努力都需要首先实现程序的动态描述,如果它是可重用的,则本质上实现动态语言。避免了需要选择不同的解决方案来解决问题,这是对许多技术的改进。
即使与已经使用某些动态语言的当前解决方案相比,也有更多不同的改进:
(此处待添加更多具体工作。)
Unilang 是为了统筹解决现有不足的新的方案中的语言部分,主要特色有:
unsafe
关键字标记“不安全”的代码段落,最基础的特性默认是“不安全”的。const
类型限定符,通过左值引用的对象允许标记为不可修改(只读),而不是如 Rust 等语言默认约定值不可变(immutable) 。unsafe
等特设语法标记“不安全”的语言中,通常会放弃语言定义的任意安全保证,而不能选择保留其中的一部分。即便忽略这个问题,语言也缺乏机制允许用户提供更严格的保证。
const
,因为键的不可变确切地由比较对象导出的等价关系定义,但类型系统无法区别两种情形。这过度地限制了键上的本应允许的操作。
const_cast
这样的不安全转换取消 const
引入的类型安全保证并自行假定不会破坏不可变性,是个无奈的变通(“更困难”的情形,且无法恢复类型安全性而效果更差)。const
的限定符机制(“更困难”的情形)。为保持通用性,Unilang 不内建提供 GUI 功能,而通过库提供相关 API 。当前计划中,Unilang 将会支持基于 Qt 的绑定的库,以便衔接过渡现有的一些桌面应用项目。Unilang 的语言设计保持足够的抽象能力和可扩展性,允许在未来直接实现 GUI 框架。
本项目包含以下文档:
README
:本文档,介绍项目的整体状况,使用方法和支持的主要功能,附更新记录。
项目的贡献者一般应能确定以上文档中的内容和对应实现的修改(若存在)的关联性。
若文档的内容之间存在逻辑矛盾或者以下不一致,请联系维护者报告缺陷。
文档不应当提供不符合被描述的对象的误导性信息。
文档通常应保持和本仓库中的其它部分(包括作为参考实现的解释器及标准库代码)一致。
但是,本项目在早期开发阶段,部分文档的一致性的要求可能存在差异:
master
分支)有限的不一致。README
中的要求。不满足以上要求的文档不一致应当视为文档或被文档描述的其它对象的缺陷。
本项目支持不同方法构建。
支持的宿主环境为 MSYS2 MinGW32 和 Linux 。
以下使用版本库根目录作为当前工作目录。
一些外部依赖项的源代码在版本库及 git 子模块中提供。
构建环境依赖以下环境工具:
git
bash
-std=c++11
的 G++ 兼容工具链,可选以下任意:
pkg-config
可选依赖:
llvm-config
安装构建环境依赖的包管理器命令行举例:
# Some dependencies may have been preinstalled.
# MSYS2 (no LLVM 7)
pacman -S --needed bash coreutils git mingw-w64-x86_64-{gcc,binutils,libffi,lld,pkgconf,qt5-base,qt5-declarative} parallel
# Arch Linux
sudo pacman -S --needed bash coreutils git gcc binutils libffi pkgconf qt5-base qt5-declarative parallel
yay -S llvm70 # Or some other AUR frontend command.
# Debian (buster/bullseye)/Ubuntu (bionic-updates/focal)/Deepin
sudo apt install bash coreutils git g++ libffi-dev llvm-7-dev pkg-config qtbase5-dev qtdeclarative5-dev parallel
LLVM 是可选的。使用非空的环境变量 UNILANG_NO_LLVM
指定构建解释器时不使用 LLVM 。基于 LLVM 的 JIT 实现会被禁用。
注释 系统可能提供的不同版本的 LLVM ,被动态加载时可能会和 LLVM 7 冲突。当前不支持混用不同版本的 LLVM 。这种情形需要指定 UNILANG_NO_LLVM
。
若系统不提供 LLVM 7 包,可能需要自行构建。本项目中,环境变量 USE_LLVM_PREFIX
指定自定义的 LLVM 的安装路径前缀,被脚本按需使用。
使用 MSYS2 工具链需注意:
-fuse-ld=lld
)。-fuse-ld=bfd
)和 LLD 。
另见以下的环境配置安装更多可选的依赖。
QT_NAMESPACE
。pkg-config
找到:
Qt5Widgets
Qt5Quick
构建之前,在版本库根目录运行以下命令确保外部依赖项:
git submodule update --init
若实际发生更新,且之前执行过 install-sbuild.sh
脚本,需清理补丁标记文件以确保再次执行这个脚本时能继续正确地处理源代码:
rm -f 3rdparty/.patched
使用以下 git
命令也能清理文件:
git clean -f -X 3rdparty
运行脚本 build.sh
直接构建,在当前工作目录输出可执行文件:
./build.sh
默认使用 g++
。环境变量 CXX
可指定要使用的其它替代,如:
env CXX=clang++ ./build.sh
若输出目录名称(参见下文)不包含 debug
则默认使用编译器选项 -std=c++11 -Wall -Wextra -O3 -DNDEBUG
,否则使用 -std=c++11 -Wall -Wextra -g
。类似地,使用环境变量 CXXFLAGS
可替代默认值。
脚本支持 LDFLAGS
指定附加的链接器选项(如 -fuse-ld=lld
),默认为空。
这个脚本使用 shell 命令行调用 $CXX
指定的编译器驱动。默认脚本会测试 GNU parallel 是否可用。若可用,则调用 parallel
命令并行构建解释器。
脚本使用以下可选的环境变量指定输出文件的位置:
Unilang_Output
:输出的解释器可执行文件路径。默认值为 unilang
。Unilang_BuildDir
:输出的中间构建文件目录。默认值为 Unilang_Output
的值所在的目录。此外,脚本支持设置以下环境变量为非空值以调整行为:
NoParallel
:跳过 parallel
命令测试并禁用并行构建。Verbose
:启用详细输出。优点是不需要进一步配置环境即可使用。适合一次性测试和部署。
利用外部工具的脚本,可支持更多的构建配置。这个方式相比直接构建脚本更适合开发。
当前 Linux 平台只支持 x86_64 宿主架构。
以下设并行构建任务数 $(nproc)
。可在命令中单独指定其它正整数的值代替。
配置环境完成工具和依赖项(包括动态库)的安装,仅需一次。(但更新子模块后一般建议重新配置。)
安装的文件由 3rdparty/YSLib
中的源代码构建。
对 Linux 平台构建目标,首先需确保构建过程使用的外部依赖被安装:
例如,使用包管理器:
# Arch Linux
sudo pacman -S freetype2 --needed
# Debian/Ubuntu/Deepin
sudo apt install libfreetype6-dev
为以下脚本中自动更新二进制依赖和对源文件补丁,需要以下依赖:
wget
7za
sed
(避免可能破坏行尾的 Win32 版本) 使用以下可选依赖可加速脚本 ./install-sbuild.sh
中的安装:
parallel
例如,使用包管理器:
# MSYS2
# XXX: Do not use mingw-w64-x86_64-sed to ensure the EOL characters as-is.
pacman -S --needed mingw-w64-x86_64-wget p7zip sed parallel
# Arch Linux
sudo pacman -S --needed wget p7zip sed parallel
yay -S llvm70 # Or some other AUR frontend command.
# Debian (buster/bullseye)/Ubuntu (bionic-updates/focal)/Deepin
sudo apt install wget p7zip-full sed parallel
运行脚本 ./install-sbuild.sh
安装外部工具和库。脚本更新预编译的二进制依赖之后,构建和部署工具和库。其中,二进制依赖直接被部署到源码树中。当前二进制依赖只支持 x86_64-linux-gnu
。本项目构建输出的文件分发时不需要依赖其中的二进制文件。
注释 脚本安装的二进制依赖可能会随构建环境更新改变,但当前本项目保证不依赖其中可能存在的二进制不兼容的部分。因此,二进制依赖的更新是可选的。但是,在构建环境更新后,一般仍需再次运行脚本配置环境,以确保覆盖安装外部工具和(非二进制依赖形式分发的)库的最新版本。其中,若二进制依赖文件不再在脚本预期的部署位置中存在,脚本会从网络重新获取最新版本的二进制依赖。
以下环境变量控制脚本的行为:
SHBuild_BuildOpt
:构建选项。默认值为 -xj,$(nproc)
,其中 $(nproc)
是并行构建任务数。可调整 $(nproc)
为其它正整数。SHBuild_SysRoot
:安装根目录。默认指定值指定目录 "3rdparty/YSLib/sysroot"
。SHBuild_BuildDir
:中间文件安装的目录。默认值指定目录 "3rdparty/YSLib/build"
。SHBuild_Rebuild_S1
:非空值指定重新构建 stage 1 SHBuild(较慢)。
3rdparty/YSLib/Tools/Scripts
的文件后,需指定此环境变量为非空值,以避免可能和更新后的文件不兼容的问题。parallel
命令启动并行构建。使用安装的二进制工具和动态库需配置路径,如下:
# Configure PATH.
export PATH=$(realpath "$SHBuild_SysRoot/usr/bin"):$PATH
# Configure LD_LIBRARY_PATH (reqiured for Linux with non-default search path).
export LD_LIBRARY_PATH=$(realpath "$SHBuild_SysRoot/usr/lib"):$LD_LIBRARY_PATH
以上 export
命令的逻辑可放到 shell 启动脚本(如 .bash_profile
)中而不需重复配置。
配置环境后,运行脚本 sbuild.sh
构建。
和直接构建脚本相比,支持并行构建,且支持不同的配置,如:
./sbuild.sh release-static -xj,$(nproc)
则默认在 build/.release-static
目录下输出构建的文件。为避免和中间目录冲突,输出的可执行文件后缀名统一为 .exe
。
此处 release-static
是配置名称。
设非空的配置名称为 $CONF
。当 $SHBuild_BuildDir
非空时输出文件目录是 SHBuild_BuildDir/.$CONF
;否则,输出文件目录是 build/.$CONF
。
当 $CONF
前缀为 debug
时,使用调试版本的库(已在先前的构建环境安装步骤中从 3rdparty
的源代码构建),否则使用非调试版本的库。当 $CONF
后缀为 static
时,使用静态库,否则使用动态库。使用动态库的可执行文件依赖先前设置的 LD_LIBRARY_PATH
路径下的动态库文件。
运行直接构建脚本使链接静态库,大致相当于此处使用非 debug 静态库构建。
使用上述动态库配置构建的解释器可执行文件在运行时依赖对应的动态库文件。此时,需确保对应的库文件能被系统搜索到(以下运行环境配置已在前述的开发环境配置中包含),如:
# MinGW32
export PATH=$(realpath "$SHBuild_SysRoot/usr/bin"):$PATH
# Linux
export LD_LIBRARY_PATH=$(realpath "$SHBuild_SysRoot/usr/lib"):$LD_LIBRARY_PATH
若使用系统包管理器以外的方式安装 LLVM 运行时库到非默认位置,类似添加 LLVM 的路径,如:
# Linux
export LD_LIBRARY_PATH=/opt/llvm70/lib:$LD_LIBRARY_PATH
以上 Linux 配置的 LD_LIBRARY_PATH
也可通过 ldconfig
等其它方式代替。
使用静态链接构建的版本不需要这样的运行环境配置;不过 LLVM 通常使用动态库。
注意 非脚本配置的外部二进制依赖项可能不兼容,需要通过系统包管理器等方式部署,依赖这些库导致解释器最终的二进制文件不保证跨系统环境(如不同 Linux 发行版)之间可移植。
运行解释器可执行文件直接进入交互模式运行 REPL ;或在命令行指定一个脚本,进入脚本模式执行脚本中的源程序。脚本名称 -
被视为标准输入。
运行解释器时使用命令行选项 -e
可在进入交互模式或脚本模式前直接求值字符串参数。选项 -e
可以使用多次,每个选项后具有一个命令行参数,这些参数字符串被作为 Unilang 源代码顺序求值。
解释器命令行支持 POSIX 约定,在命令行参数 --
之后的其它参数不被解释为选项。这允许指定和选项重名的脚本文件。
命令行选项 -h
或 --help
显示解释器命令行的帮助。
解释器处理以下可选环境变量:
ECHO
:非空值启用 REPL 回显。这确保解释器在每个交互会话后输出求值结果。UNILANG_NO_JIT
:非空值停用基于 JIT 编译的代码执行优化,使用纯解释器。UNILANG_NO_SRCINFO
:非空值停用用于诊断消息输出的从源文件取得的源代码信息。源文件名仍被诊断消息使用。UNILANG_PATH
:指定库加载路径。详见语言规范对标准库函数 load
的说明以及解释器实现对标准库模块操作的说明。 除使用选项 -e
,配合外部的 echo
命令,也可支持非交互式输入,如:
echo 'display "Hello world."; () newline' | ./unilang
示例中包含使用 QtWidgets 的程序。
./unilang demo/qt.txt
等价的 Python 实现参考 demo/qt.py
。
另一个使用 QtQuick 的示例类似 Qt 官方 qmlscene
工具的最小化版本:
./unilang demo/qml.txt
这个示例加载相对当前工作目录的源文件 demo/hello.qml
。当前工作目录可以是存储库的根目录。
./unilang demo/quicksort.txt
文件 test.sh
是测试脚本。可以直接运行测试用例。脚本在其中调用解释器。
测试用例直接在脚本代码中指定,包括调用解释器运行测试程序 test.txt
。在 REPL 中 load "test.txt"
也可加载测试程序。
脚本以下支持环境变量:
UNILANG
:指定解释器可执行文件路径,默认为 ./unilang
。PTC
:非空时,运行 PTC 测试用例。手动终止进程后结束用例。在此期间,正确的 PTC 实现可确保最终内存占用不随时间增长。 使用 sbuild.sh
构建的可执行文件不在当前目录。可使用类似以下的 bash
命令调用:
env UNILANG=build/.debug/unilang.exe ./test.sh
文件 valgrind.sh
通过 valgrind
调用解释器,用于测试。
脚本默认使用 Callgrind 收集性能数据。可使用 kcachegrind
查看数据。
使用环境变量指定被调用的解释器的命令和控制具体调用的选项,如:
env UNILANG=build/.release-l/unilang.exe OUTPUT=build/.release-l/callgrind.out LOGOPT=--log-file=build/.release-l/callgrind.log ./valgrind.sh -e ''
若没有显式指定,变量可具有默认值。上述命令实际上等价直接调用 ./test-valgrind.sh -e ''
而不显式指定任何环境变量。
其它被支持的变量和变量的默认值参见脚本源代码。
语言特性可参照《Unilang 介绍》中的例子(尚未完全支持)和特性清单。
另见语言规范和解释器设计和实现文档。
不精确数使用 C++ 标准库 <cstdio>
兼容格式输出,可能在非默认区域(locale) 设置中输出非预期的格式,如:
.
。当前版本在非默认区域下不确保这些输出能被作为 Unilang 数值字面量解析。
我们鼓励您报告问题和贡献修改。
基本事项参见开发者代码贡献指南。
但是,以下可能不同的规则优先适用于本项目:
[可选的范围: ]<描述>
。
:
仍然要求是英文标点,从属于可选的范围描述中。
。
u8
字面量的规则,使一些代码含义可能不同而不应当在此被使用。-std=c++11 -pedantic-errors
。{
和 }
应单独占一行,但构成 braced-init-list 或 lambda-expression 的最外层块时除外。
INC_Unilang_
保留给头文件守卫宏。PascalCase
。namespace
指令(仅允许在实现内部使用 using namespace
)。typedef
。
using
代替。\
而不是 @
。*
和 /
。
//!
而不是 ///
。en-US
。en-US
。.
一并出现在文件名中的在扩展名(及前缀 .
前,若存在)前一次。-
和 ISO 3166-1 代码的序列构成。en-US
版本的文件名省略 .en-US
。例如,README.md
是 en-US
区域的翻译版本的文件名。其对应 zh-CN
版本的文件名是 README.zh-CN.md
。Unilang 在 BSD-2-Clause-Patent 下发布。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。