# 概述
NPL 是 YSLib 提供的语言集合,它在语言规范层次上被设计为可扩展的。
通过*派生(derive)* 现有的语言( NPL 的*方言(dialect)* ),避免完全重新设计新的语言,来满足需要一些新语言的场合下的需求。被派生的语言是 NPL 的[抽象语言实现](../Terminology.zh-CN.md#规范)。翻译或执行 NPL 或 NPL 抽象语言实现的程序是 NPL (方言)的[具体语言实现](../Terminology.zh-CN.md#规范)。具有具体语言实现的方言仍可以派生新的语言作为[领域特定语言](https://en.wikipedia.org/wiki/Domain-specific_language)。
派生领域特定语言的一个其它的例子是 [XML](https://en.wikipedia.org/wiki/XML) 。
## 语法和语义
NPL 的[语法](#语法)基于形式文法上可递归构造的**表达式**。
在操作语义(基于[项重写系统](#项重写系统))的[意义](#规约规则和求值)上,其中的子表达式又称为**项**。
非正式地,NPL 使用类似 [S-表达式](https://zh.wikipedia.org/wiki/S-%E8%A1%A8%E8%BE%BE%E5%BC%8F)的基本语法,但不使用二元有序对(和终止符号)而直接支持列表;即表达式直接以是否为括号作为边界,分为[列表表达式](#列表表达式)和非列表表达式。
正式语法中,作为子表达式的项可以是列表或非列表项的[复合表达式](#复合表达式)规约列表项中的一部分代替有序对的地位,以要求任意项可被无歧义地进行从左到右的语法分析。
NPL 只要求小括号作为列表表达式的边界。其它替代的括号由[派生实现](#略称)约定。对于适用于多个 NPL 实现的可移植代码,避免使用其它语言中习惯作为代替括号边界的字符表示替代的括号以外的含义,特别地,\[ISO C++] 文法 `balanced-token` 中的边界字符 `()[]{}` 。
NPL 对标识符的限制较为宽松。\[ISO C] 和 \[ISO C++] 的所有标识符都是 NPL 标识符。但派生实现可加以限制。
NPL 不提供专用的注释语法。以特定形式的项(如表示字符串的字面量)替代注释是预期的惯用法(idiom) 。这不妨碍派生语言可能添加预处理器扩展特性。
NPL 提供了一些通用的概念和[公共规则](#npl-公共语义),但不构成[具体语言实现](../Terminology.zh-CN.md#规范)的完整语义规则。语义规则由派生实现补充完整。
**注释**
排除注释及[中缀标点 `;` 和 `,`](#中缀语法) ,NPL 的语法和 [Scheme 语言](https://en.wikipedia.org/wiki/Scheme_%28programming_language%29)或[Kernel 语言](https://web.cs.wpi.edu/~jshutt/kernel.html)的语法近似。不过,NPL 不支持构造循环引用,也不提供相关语法。
## 需求概述
设计满足的需求描述参见[这里(en-US)](https://github.com/FrankHB/pl-docs/blob/master/en-US/calling-for-language-features.md) 。
需求来源:
* 出发点:构建一个可用计算机实现的语言。
* 基本目的:在以标准 C++ 环境( \[ISO C++] 定义的*宿主实现(hosted implementation)* )的程序框架中嵌入配置和脚本操作。
* 扩展目的:渐进地向独立的计算机系统演进,探究能适用于各个领域并以计算机实现的*通用目的语言(general-purpose language)* 。
本文档描述基于此出发点的 **NPL(Name Protocoling Language)** (一个替代的递归缩写是“NPL's not a Programming Language”,因其不仅适合作为 PL 的[元语言](../Terminology.zh-CN.md#程序设计语言)特性及其参照实现。
和大部分其它设计不同,为了确保一定程度的适应通用目的的性质,它们被设计整体首要考虑。这样的设计的语言是(自设计(by desing) 用于)满足通用目的的语言(general-purposed language) 。
## 其它设计和实现参考
NPL 是独立设计的语言,但它和 \[RnRK] 定义的 [Kernel 语言](https://web.cs.wpi.edu/~jshutt/kernel.html)有许多核心设计的相似之处,尽管设计的一些基本特征(如[资源可用性基本约定](#资源可用性基本约定))以及[基本哲学](#领域设计原则)相当不同。
NPL 的[主要实现](#npla1-核心语言)的核心部分实质上支持了 Kernel 的[形式模型](#模型)—— vau 演算(vau calculi) 。
**注释** 另见[操作语义](#形式语义方法)。
具体的 NPL 语言在这些模型的基础上提供。
[NPL 的命名](#需求概述)即体现了 vau 演算和传统 [λ 演算](#λ-完备语义和对应语法)为模型的语言的核心差异:
强调允许在[对象语言](../Terminology.zh-CN.md#程序设计语言)中指定求值上下文的*显式求值(explicit evaluation)*(而非 Lisp 方言中以 `quote` 为代表的显式干预默认的隐式求值)的风格以及[表达式](#表达式)[求值](#基本语义概念)前后的不同,特别地,关注在语言中直接表达的[名称](../Terminology.zh-CN.md#程序设计语言)和[求值](#基本语义概念)后指称的[实体](../Terminology.zh-CN.md#程序设计语言)的不同。
更进一步地,NPL 普遍地支持区分[一等引用](#一等引用)和被引用的[一等实体](#基本语义概念)并具有更精确的资源控制机制,这是与 Kernel 的主要设计上的差异。
关于 vau 演算的形式模型和其它相关内容,详见 \[Shu10] 。特别地,vau 演算提供了 [fexpr](https://en.wikipedia.org/wiki/Fexpr) 类似的抽象。
**注释** 另见[求值算法设计的实例](#npla1-规范求值算法)。
关于一些其它支持 fexpr 特性的语言设计,参见:
* [PicoLisp](https://software-lab.de/doc/faq.html#lambda)
* [newLISP](http://www.newlisp.org/downloads/newlisp_manual.html#define-macro)
和 Kernel 以及本设计不同,这两个例子的设计使用[动态作用域](#函数和函数应用的求值环境);在主要的特性中存在一些关键的不同而在形式模型的适用性上有显著的区别。
**注释**
NPL 和历史上同名的 [John Darlington](https://en.wikipedia.org/wiki/John_Darlington) 的 [NPL (New Programming Language)](https://en.wikipedia.org/wiki/NPL_(programming_language)) 没有直接渊源;特别地,后者的多个等式的函数定义语法和高阶类型没有被内建支持,而[静态类型](#类型系统和类型机制)和纯函数式限制被避免。
Kernel 语言的原始参照实现是 SINK 依赖 MzScheme version 103 的解释器实现,和 \[RnRK] 有一定差异。例如,字面量 `#ignore` 和 `#inert` 用 `%ignore` 和 `%inert` 代替。
*[klisp](https://web.archive.org/web/20210301121505/http://klisp.org/)* 是 Kernel 语言的一个更完善的实现。
有些特性(如复数支持)都没有在这两个中实现提供,而仅在 \[RnRK] 中指定。
## 实现
在 YFramework/NPL 提供一些参考[具体语言实现](../Terminology.zh-CN.md#规范)。当前 YFramework 主要使用[抽象语言实现](../Terminology.zh-CN.md#规范) [NPLA](#npla) 的具体*派生(derived)* 的实现 [NPLA1](#npla1-核心语言) ,在这个基础上用于不同的目的,如[程序配置](../Tutorial/Configuration.zh-CN.md)、[动态加载的 GUI](../Tutorial/GUI.zh-CN.md) 等。
NPLA 提供了比大多数现有的程序设计语言更强大的一般抽象。这集中体现在:
* 和 NPL 的原始设计一致,不提供也不要求区分实现的阶段(phase) 。
* 支持[一等环境](#npla-环境)不修改现有语言的[求值算法](#求值规约)即可实现共享类似语法的新语言。
* 允许以一般手段表达求值和未求值表达式的差异。
这意味着 NPLA 是本质上动态的语言,但和一般语言不同,用户可以很大程度上动态地替换现有语言实现,包括在运行时替换一个解释实现为一个或多个优化编译器。这也意味着语言设计上既不需要区分解释实现和编译实现(本质上不对立),也不需要区分动态和静态(因为随时能从基础语言上构造出静态子集)。
这样的特性设计在绝大多数语言中不存在并且几乎无法支持。已知唯一的例外是 [Kernel](http://web.cs.wpi.edu/~jshutt/kernel.html) ,在这些特性上有极大的相似,尽管实际上基本特性是**独立设计的**,并且在基本[设计哲学](#领域设计原则)上有极大不同( NPL 和 \[RnRK] 中明确的 guidelines有很大不同且基本不兼容)。不过,考察设计的完整性,NPL 的派生语言也从中借鉴了一些重要的设计:
* [`$vau`](http://lambda-the-ultimate.org/node/4093) 、*合并子(combiner)* /*应用子(applicative)* /*操作子(operative)* 等术语。
* 在[绑定构造](#绑定构造)中支持模式匹配的[形式参数树](#绑定匹配)。
* 一些以合并子形式提供的操作。
* 一般的作为接口提供的合并子在 NPL 中仍称为函数;合并子是作为表达式的函数的特定的[求值结果](#基本语义概念)。
* **注释** 在 Scheme 中,合并子中的应用子对应[过程](#过程)。
* 相似的操作主要体现在[名称](../Terminology.zh-CN.md#程序设计语言)和[语义](../Terminology.zh-CN.md#自指)上。因为一些基本设计的差异,不保证完全兼容。
* 相似操作的实现不尽相同,但其中不通过[宿主语言](#实现的执行阶段)的直接的实现(称为*派生(derivation)* )有一部分几乎完全相同。
* 使用 `$` 作为一些[标识符](#基本词法构造)(如 `$lambda` )的前缀是独立设计的巧合;现在含义已和 Kernel 一致,表示 *special form* 。
一些值得注意的和类似语言的主要设计差异(原理详见开发文档):
* NPL 和 Kernel 类似,强调[一等对象](#基本语义概念),但含义有所不同。此处的“对象”和 \[ISO C] 及 \[ISO C++] 中的定义类似,具有比 Kernel 更严格的含义。
* 和 Kernel 合并子及 Scheme 过程类似,NPLA 默认使用[按值传递](#求值策略)参数和结果;但与之不同,不隐式对实际参数[别名](#对象别名),不[共享](#共享引用)对象。
* NPLA 和[派生实现](#略称)的语法和整体的求值类似 Scheme 和 Kernel 大多数基于 S-表达式的 [Lisp](https://en.wikipedia.org/wiki/Lisp_%28programming_language%29) 方言,但有一些显著的区别。
* 和 Scheme 不同,而和 Kernel 一致,NPLA 避免顶层(top-level) 和局部的上下文的差异。
* NPLA1 明确区分约定包括列表项的求值规则。和传统习惯不同,NPLA1 中括号明确不需要表示应用的含义,这可以减少一些场合(如命令行)需要输入的过多的连续括号。
* 和 \[RnRS] 定义的 Scheme 以及 \[RnRK] 定义的 Kernel 一致,不支持某些 Lisp 方言的方括号 `[]` 替代圆括号 `()` 的语法。
* 不提供注释语法。
* 语言实现中[内置预处理处理中缀 `;` 和 `,`](#中缀语法) ,作为前缀合并子 `$sequence` 和 `list` 的语法糖。两者的含义和 Kernel 中的相同(类似 Scheme 的 `begin` 和 `list` )。
* 和 Kernel 相似而和 Scheme 不同,使用操作子及一等环境和 `eval` 代替 Scheme 的[卫生宏(hygienic macro)(en-US)](https://en.wikipedia.org/wiki/Hygienic_macro) 及宏展开的作用。
* 和 Kernel 类似,鼓励使用直接求值风格而不是[引用(quote)](#求值算法实现风格) 。
* 不过 NPLA 也提供了 `$quote` 的派生而非如 Kernel 一样完全避免。
* 和 Kernel 不同,NPL 明确支持资源抽象,**不保证支持**[循环引用](#自引用数据结构和循环引用),而 NPLA 明确**不支持**循环引用。
* NPLA 明确支持基于 \[ISO C++] 实现的对象模型和[互操作](../Terminology.zh-CN.md#规范),且明确**不要求**支持全局 [GC](https://zh.wikipedia.org/zh-cn/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6_%28%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8%29) 。
* 从在互操作的目的出发,和 C++ 具有相似性和相容性。
* 支持基于 [C++ 抽象机语义](https://eel.is/c++draft/intro.abstract)的更一般的[副作用](#基本语义概念)(除原生 `volatile` 外)和一等状态(first-class states) 。
* 和 \[ISO C++] 类似,在[语言规则](../Terminology.zh-CN.md#规范)中保留特定的[未定义行为](../Terminology.zh-CN.md#程序设计语言),而不要求实现避免。
* 暂时不直接支持多线程环境,但可以在不同线程上同时使用不同的实现的实例。
* 函数默认使用不隐式别名的按值调用和返回传递复制或转移值,和 C++ 对应上下文的复制初始化(copy initialization) 语义一致。(不过求值为操作子的 NPL 函数在 C++ 没有直接的对应。)
* 在 vau 演算的论文 (\[Shu10]) 中,提及不支持全局 GC 有较大的管理开销(admistrative cost) 但没有详细讨论和[语言特性](../Terminology.zh-CN.md#规范)的联系。
* 即便不支持全局 GC ,当前实现仍然**明确支持** [PTC(proper tail call)](#尾调用和-ptc) 。
* PTC 基于语言规则而不是[实现行为](#实现行为)定义,详见 [proper tail recursion](https://www.researchgate.net/profile/William_Clinger/publication/2728133_Proper_Tail_Recursion_and_Space_Efficiency/links/02e7e53624927461c8000000/Proper-Tail-Recursion-and-Space-Efficiency.pdf) ,这里和 Kernel 提供的保证含义一致。
* 没有在其它语言发现这种不支持全局 GC 和支持类似 C++ 副作用的情形下的 PTC 支持的先例。
* 和 Kernel 不同,NPLA 不完全强制对象类型的[封装](#封装);且基于支持互操作的考虑,支持[开放](#类型全集)的[类型系统](#类型系统和类型机制),而不要求覆盖所有值(即要求对象类型分区(partition) )。
* 对机器数(不论是整数还是浮点数)的操作被剥离了,当前不被支持,需要用户代码添加个别操作。
* NPLA 的[规约](#基本语义概念)框架和 vau 演算的[操作语义](#形式语义方法)几乎完全一致,不过实际上(因为先前语言设计上的不确定)显著地保留了更多的可扩展和可修改性。
## 当前具体实现
当前派生实现的 [NPLA1](#npla1-核心语言) 由 YFramework 提供 [API](../Terminology.zh-CN.md#程序设计语言) 。其中包括 [REPL (read-eval-print loop)](https://zh.wikipedia.org/wiki/%E8%AF%BB%E5%8F%96%EF%B9%A3%E6%B1%82%E5%80%BC%EF%B9%A3%E8%BE%93%E5%87%BA%E5%BE%AA%E7%8E%AF) 的解释实现。外部文件的形式的 NPLA1 脚本可被基于这些 API 实现的 [stage 1 SHBuild](../Tools/SHBuild.zh-CN.md) 调用并用于 YFramework 的构建。
由 YFramework 对外部编码的假设,NPLA1 实现加载的文件流的剩余内容的编码视为 UTF-8 ;同时支持 CR+LF 或 LF 为换行符。
**注释** 这些实现基于 YSLib API 提供互操作支持。
# 绪论
## 正式引用
仅在此给出本文档中的外部引用的名称。其它引用文献的内容详见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
* \[ISO C] [ISO/IEC 9899](https://www.iso.org/standard/74528.html)
* \[ISO C++] [ISO/IEC 14882](https://www.iso.org/standard/79358.html)
* \[ISO C++11] [ISO/IEC 14882:2011](https://www.iso.org/standard/50372.html)
* \[ISO C++14] [ISO/IEC 14882:2014](https://www.iso.org/standard/64029.html)
* \[ISO C++17] [ISO/IEC 14882:2017](https://www.iso.org/standard/68564.html)
* \[ISO C++20] [ISO/IEC 14882:2020](https://www.iso.org/standard/79358.html)
* \[WG21] (ISO/IEC JTC1/SC22/WG21) [C++ Standards Committee Papers](https://www.open-std.org/jtc1/sc22/wg21/docs/papers)
* \[WG21 P0135R1] Richard Smith, [Wording for guaranteed copy elision through simplified value categories](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0135r1.html), 2016-06-20.
* \[RnRK] [Revised Report on the Kernel Programming Language](https://web.cs.wpi.edu/~jshutt/kernel.html)
* \[Shu09] John N. Shutt, [Revised-1 Report on the Kernel Programming Language](ftp://ftp.cs.wpi.edu/pub/techreports/pdf/05-07.pdf), Technical report WPI-CS-TR-05-07, Worcester Polytechnic Institute, Worcester, MA, March 2005, amended 29 October 2009.
* **注释** 当前 \[RnRK] 只有 n = -1 的版本。引用确切版本时,同 \[Shu10] 中的用法,使用 \[Shu09] 标记;但引用时一般同样不涉及其版本差异。
* \[Shu10] John N. Shutt, [Fexprs as the basis of Lisp function application; or, `$vau`: the ultimate abstraction](https://web.wpi.edu/Pubs/ETD/Available/etd-090110-124904/unrestricted/jshutt.pdf), Ph.D. Dissertation, WPI CS Department, 2010.
* \[RnRS] [Revised Report on the Algorithmic Language Scheme](http://www.scheme-reports.org/)
* \[R5RS] [Revised5 Report on the Algorithmic Language Scheme](https://schemers.org/Documents/Standards/R5RS/r5rs.pdf)
* \[R6RS] [Revised6 Report on the Algorithmic Language Scheme](http://www.r6rs.org/final/r6rs.pdf)
* \[R6RS-Rationale] [Revised6 Report on the Algorithmic Language Scheme -Rationale-](http://www.r6rs.org/final/r6rs-rationale.pdf)
* \[R7RS] [Revised7 Report on the Algorithmic Language Scheme](https://small.r7rs.org/attachment/r7rs.pdf)
* \[Fl91] Matthias Felleisen, [On the Expressive Power of Programming Languages](https://www.ccs.neu.edu/racket/pubs/scp91-felleisen.ps.gz), _Science of Computer Programming_ [Volume 17, Issues 1–3](https://www.sciencedirect.com/journal/science-of-computer-programming/vol/17/issue/1), December 1991, pp. 35–75.
* \[EGAL] James Noble, Andrew P. Black, Kim B. Bruce, Michael Homer and Mark S. Miller, [The Left Hand of Equals](http://web.cecs.pdx.edu/~black/publications/egal.pdf), Onward! 2016: _Proceedings of the 2016 ACM International Symposium on New Ideas, New Paradigms, and Reflections on Programming and Software_, October 2016, pp. 224–237.
* \[So90] Harald Søndergaard and Peter Sestoft, [Referential transparency, definiteness and unfoldability](http://www.itu.dk/people/sestoft/papers/SondergaardSestoft1990.pdf), [_Acta Informatica_](https://link.springer.com/journal/236) 27, 1990, pp. 505–517.
* \[Rust] [The Rust Reference](https://doc.rust-lang.org/reference/index.html)
* 不定期更新。
* \[Fi94] Andrzej Filinski, [Representing Monads](https://dl.acm.org/doi/pdf/10.1145/174675.178047), POPL '94: _Proceedings of the 21st ACM SIGPLAN-SIGACT symposium on Principles of programming languages_, February 1994, pp. 446–457.
* \[Hi90] Robert Hieb, R. Kent Dybvig and Carl Bruggema, [Representing Control in the Presence of First-Class Continuation](https://legacy.cs.indiana.edu/~dyb/pubs/stack.pdf), _ACM SIGPLAN Notices_, Volume 25, Issue 6, Jun. 1990, pp. 66–77.
* \[Racket] [Racket Documentation](https://docs.racket-lang.org/)
* 不定期更新。
* \[Chu41] Alonzo Church, [_The Calculi of Lambda-Conversion_](https://compcalc.github.io/public/church/church_calculi_1941.pdf), Annals of Mathematics Studies, Princeton: Princeton University Press, 1941.
* \[Bare84] Hendrik Pieter Barendregt, [_The Lambda Calculus: Its Syntax and Semantics_](https://philpapers.org/rec/BARTLC) \[_Studies in Logic and the Foundations of Mathematics_ 103], Revised Edition, Amsterdam: North Holland, 1984.
* \[Cl98] William D. Clinger, [Proper Tail Recursion and Space Efficiency](https://www.researchgate.net/profile/William_Clinger/publication/2728133_Proper_Tail_Recursion_and_Space_Efficiency/links/02e7e53624927461c8000000/Proper-Tail-Recursion-and-Space-Efficiency.pdf)
* \[IEC 60559] [ISO/IEC 60559](https://www.iso.org/standard/80985.html)
* \[ECMAScript] [ECMA-262](https://www.ecma-international.org/publications-and-standards/standards/ecma-262/)
* \[ECMAScript 2019] [ECMA-262 10.0](https://262.ecma-international.org/10.0/)
## 领域设计原则
本节描述被本文档中的一些原理讨论引用的的公共依据。
原则指关于设计和实现的哲学,同时作为一般规则约束设计和实现的工程阶段。
关于需求特别是通用目的语言的讨论,参见[需求概要(en-US)](https://github.com/FrankHB/pl-docs/blob/master/en-US/calling-for-language-features.md#module) 。
### 本体论
为使论述有效,约定*本体论(ontology)* 规则。
基本的本体论规则是约束逻辑系统构造的公理。
#### 正规性
有效的陈述(如需求描述)应保证操作上可预期结果。
在此意义下,缺乏约束性的规则不可预期的风险是代价。
推论:规则应适当约定适用范围,以避免外延不清。
#### 存在性
语义的存在体现本质。
仅仅应用语法规则,即限定为*语法的文法(syntactic grammar)* 的形式系统归纳的设计,不被视为表示任何[有效](#正规性)的含义。
#### 名实问题
名义概念的内涵和外延应被足够显式指定,避免指涉上的歧义,允许构造[有效的陈述](#正规性)。
#### 不可分的同一性
[不可分的同一性(the identity of indiscernibles) (en-US)](https://plato.stanford.edu/entries/identity-indiscernible/) 比较陈述的客体之间是否相同而不需要被重复地处理。
### 价值观
价值观是关于价值判断的规则,其输出为二元的值,决定是否接受决策。
作为应对普遍需求场景的不同解决方案选型时的价值判断的抽象归纳,价值观被作为比较是否采用设计相关决策的全局依据。
以下陈述形式表达价值优先的选项,同时作为公理。
**注释** 相同推理结果仍然可能不唯一,这来自于自然语言描述的输入的不精确性。
#### 变化的自由
在明确需求的前提下,尽可能保证对现状按需进行改变的可行性和便利性。
适用于一般需求。
对计算机软件或其它可编程的实体:尽可能避免不必要地损失可修改性,便于保障按需引入或除去[接口](../Terminology.zh-CN.md#非自指)及其实现的自由。
**原理**
一般地,需求可能随着不可控的外部条件变化。假设已明确的需求不变只能适合相当有限的情形。积极应对变化能提供价值。
#### 避免不必要付出的代价
尽可能消除对满足需求无意义的代价,减少影响需求实现的整体成本。
适用于一般需求中设计决策的比较。
对计算机软件或其它可编程的实体:不为不需要的[特性](../Terminology.zh-CN.md#规范)付出代价。
**注释**
一个类似的表述:
> Efficiency has been a major design goal for C++ from the beginning, also the principle of “zero overhead” for any feature that is not used in a program. It has been a guiding principle from the earliest days of C++ that “you don’t pay for what you don’t use”.
— [ISO/IEC TR 18015](https://www.iso.org/standard/43351.html)
#### 最小接口原则
在满足需求的前提下,尽可能使用符合倾向减小实现需求代价的单一良基关系下具有极小元的接口设计。
**注释** 减小实现需求的代价,如减小设计工作量。
这是一条模式规则,依赖具体情形何者符合良基关系的极小元这条非模式规则作为输入。
实际使用时,非模式规则可以直接指定为二元关系的子集,或者一种良序的度量。
**注释** 例如,“公开函数声明数”“模块数”。
这个输入也可能直接对应符合需求集合的某种最小功能集合而不需要附加度量,如表示某种设计的裁剪。
注意规则指定的基数是对实现需求有意义的代价,因此不涵盖[避免不必要付出的代价](#避免不必要付出的代价)。
在确定的范围内尽可能少地提供必须的接口,避免不必要的假设影响接口适应需求的能力,同时减少实现需求的各个阶段的复杂性。
适用于一般需求的实现,特别地,强调“通用”目的时。
对需要在计算机上实现的人工语言设计:设计语言不应该进行功能的堆砌,而应该尽可能减少弱点和限制,使剩下的功能显得必要。
> 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.
— \[RnRS] & \[RnRK]
**注释**
其它各个领域中的实质等价一些表述包括:
* 用于安全系统设计的[最小特权原则](#最小特权原则)。
* 用于自然科学理论设计的[奥卡姆剃刀(Occam’s Razor)](https://zh.wikipedia.org/zh-cn/%E5%A5%A5%E5%8D%A1%E5%A7%86%E5%89%83%E5%88%80) 原理,避免不必要的假设引入诉诸无知(argument from ignorance) 谬误。
#### 关注点分离原则
[关注点分离(separation of concerns, SoC)](https://zh.wikipedia.org/zh-cn/%E5%85%B3%E6%B3%A8%E7%82%B9%E5%88%86%E7%A6%BB) 原则 :局部设计的内容应和需求的陈述或其它隐含的设计前提分别一一对应。
适用于一般需求的实现,特别是其中依赖认识论观点的过程。
**原理**
这条规则利用需求和设计内容陈述中概念外延普遍蕴含的*局域性(locality)* ,提供给定代价下更多的可行性或求解给定问题时使用较小的代价,用于:
* 应对不可控复杂条件下使问题可解。
* 局部可复用现有解的子集。
此外,尽管并非总是必要,应用知识内容的简单假设、[最小接口原则](#最小接口原则)和本原则可在认识论上导出还原论。
### 形而上学
根据作为需求的[价值观](#价值观),归纳适用于通用目的语言应有的构成及其性质(形而上学(metaphysics) )的设计规则,包括三条价值判断实现公理:
* 设计应尽可能满足[正确性](#正确性)。
* 设计应尽可能满足[简单性](#简单性)。
* 设计的正确性应优先于简单性。
具备这些性质的设计可视为由[价值判断](#价值观)蕴含,预设前提为真的设计[方法论](#方法论)的实现。
**注释**
注意和 [worse is better](https://en.wikipedia.org/wiki/Worse_is_better) 或 [the MIT approach](https://en.wikipedia.org/wiki/Worse_is_better#The_MIT_approach) 不同,设计的性质并非完全并列。特别地,[完整性](#完整性)和[一致性](#一致性)都被作为正确性的一部分考虑。
因为[变化的自由](#变化的自由),具体需求以及判断正确性和简单性的确切依据都可能会随着项目的进展而变化。
#### 正确性
设计应正确地反映需求,不在需求的基础上新增作为实现细节以外的不确定性。
无法确保满足这种*正确性(correctness)* 要求时,不应继续设计。
正确性包含*可行性(feasibility)* 。
若无法满足正确性,则需求输入存在问题。
正确性不包含但应逻辑蕴含设计的一些其它性质。若无法实现,则具体性质的定义存在问题。
保持正确性作为设计评价的首要依据以使决策简单,同时能符合[价值判断](#价值观)。
##### 完整性
正确性应蕴含*完整性(completeness)* ,即确保没有需求被遗漏。
推论:设计应包含完整的需求响应。
**原理**
对通用编程语言的一个完整性要求是支持[计算](../Terminology.zh-CN.md#计算机科学)上的*可表达性(expresiveness)* 。
这种性质被称为*可有效计算性(effective computability)* ,或 *Turing 完备性(Turing completeness)* 。在可物理实现的计算普遍遵循 [Church–Turing 论题(Church–Turing thesis)](https://zh.wikipedia.org/zh-cn/%E9%82%B1%E5%A5%87%EF%BC%8D%E5%9B%BE%E7%81%B5%E8%AE%BA%E9%A2%98) 的情形下,这同时是*可计算性(computability)* 。以上性质一般不加分辨。
具体的语言中允许的表达的可计算性是*表达能力(expressive power)* 。另见 \[Fl91] 。
特定的场合要求更弱的性质。例如,类型检查等情形需要*全(total)* 计算而确保实现总是[可终止](#计算复杂度约定)。这种要求在完整的实现中可通过附加的设施(用户提供的标注或证明)保证,而不应通过系统设计的规则静态地排除,否则实现是不完整的。仅在作为领域特定语言时,通过从需求中排除可计算性,静态规则作为优化是被允许的。
##### 一致性
正确性应蕴含一致性,即内部的逻辑无矛盾性。
推论:设计应保证一致性。
#### 简单性
在满足正确性的前提下,接口设计应尽可能满足简单性(simplicity),即尽可能少地具有可被继续简化的内容。
接口设计的简单性优先于实现的简单性。
#### 可修改性
*可修改性(modifiablity)* :在满足需求的前提下,修改应尽可能少地有碍于其它的接口。
这是[变化的自由](#变化的自由)的推论。
#### 避免抽象泄漏
*泄漏的抽象(leaky abstraction)* 指抽象的底层复杂性没有被抽象合理地[隐藏](#信息隐藏),而在一定程度上构成了利用抽象时的不必要的依赖。
这种*抽象泄漏(abstraction leak)* 的结果直接和[避免不必要付出的代价](#避免不必要付出的代价)、[关注点分离原则](#关注点分离原则)和[简单性](#简单性)冲突。
同时,抽象的有效性被削弱,泄漏构成的不被预期的依赖难以满足[正确性](#正确性);只要有避免抽象泄漏的方法,就不满足[最小接口原则](#最小接口原则)。
因此,只要可能,避免抽象泄漏。
**注释** 在信息安全意义上,抽象泄漏还可能提供难以抵御的附加的攻击信道。
#### 关注资源限制
为了可实现性,*宿主(host)* 系统对总的资源(典型地,运行程序需要的存储)有[未指定](../Terminology.zh-CN.md#程序设计语言)的上限。
除此之外,接口抽象不附加接口语义要求以外的限制。
这个原则同时利于满足[正确性](#正确性)和[简单性](#简单性)。而不遵循这个原则的设计在接口描述上违反[最小接口原则](#最小接口原则)。
在允许实现的前提下,附加具体[特性](../Terminology.zh-CN.md#规范)上的使用限制(如 \[ISO C] )可放宽对实现的要求;但无原则地随意选取此处的限制不足以直接证明具体的限制的有效性,而依赖实际实现的情况才能判断,造成[抽象泄漏](#避免抽象泄漏)。
**注释** 实例:[PicoLisp](https://software-lab.de/doc/faq.html#lambda) 使用符合此原则的设计。
#### 开放性
*开放性(openness)* :除非另行指定,不假定实体不存在。
这个原则主要用于建模(modeling) 的依据。对一般的模型,这个原则称为[开放世界假定(open-world assumption)](https://zh.wikipedia.org/zh-cn/%E5%BC%80%E6%94%BE%E4%B8%96%E7%95%8C%E5%81%87%E5%AE%9A)。
与之相对,[封闭世界假定(closed-world assumption)](https://zh.wikipedia.org/zh-cn/%E5%B0%81%E9%97%AD%E4%B8%96%E7%95%8C%E5%81%87%E5%AE%9A) 需要提前设置一个*全集(universe)* 以保持至少在逻辑的意义上[合规](#正规性)。
开放世界的元素的全集是模型的结构化规则推断得到的,而非名义上的定义决定。这同时称为模型的语言的*论域(universe of disclosure)* 。
**原理**
封闭世界假定表面上可能简化实现,但在一般的模型中是不必要的,因为保持问题合规性的论域应已由清晰的需求描述规范,不应为此阻碍实现[变化的自由](#变化的自由)。
使用封闭世界假定的一个主要实用意义是使模型在有限的信息下能推理出逻辑上更强的结论。在重视结论的知识系统中,这通常是一种优化;但在重视[表达能力](#完整性)(而通过其它方式辅助求解问题)的通用模型中,这种前提是一种直接的限制。同时,封闭世界假定的优化不保证对所有输入有效,对否定输入还可能导出一些矛盾。
**注释**
开放世界包含的元素的外延及其语言的论域伴随随[语言规则](../Terminology.zh-CN.md#规范)的修改而改变。
开放世界不限制论域中的某个子集是封闭的。例如,论域中可能存在某个子集的所有元素通过一定方式被枚举。
### 结构和依赖原则
#### 接口设计和实现分离
语言设计独立于[语言实现](../Terminology.zh-CN.md#规范)。
这是同时应用[最小接口原则](#最小接口原则)和[关注点分离原则](#关注点分离原则)的推论。
这种分离允许[避免抽象泄露](#避免抽象泄漏)。
典型地,使用提供接口抽象层作为必要构造的架构方法,即分层设计。
#### 最小特权原则
[最小特权原则(principle of least privilege, PoLA)](https://zh.wikipedia.org/zh-cn/%E6%9C%80%E5%B0%8F%E6%9D%83%E9%99%90%E5%8E%9F%E5%88%99) :除非有必要,接口抽象不提供满足需求以外的其它信息和资源。
这是[最小接口原则](#最小接口原则)在限制适用领域前提下的等价表述之一,用于避免不必要的访问路径引入额外的安全(safety) 风险,更容易满足(针对恶意使用风险的)安全性(security) 和可信性保证相关的需求。
实质上提供例外的必要性之一是接口正确性:不附加不存在于需求以外的安全设计;根据[可修改性](#可修改性),这应是实现细节。
#### 最小依赖原则
最小依赖原则(principle of least dependencies) :除非有必要,接口实现仅使用必要的依赖。
这是[最小接口原则](#最小接口原则)的推论之一,其非模式规则的输入为:
* 已知必要的依赖较已知必要的依赖和不必要的依赖的并集要求较小的使用和维护成本。
* 这里的使用包括演绎抽象自身的推理(reasoning) 。依赖较少时,推理时需要搜索的解空间也越小。
##### 单一模块依赖倒置原则
依赖倒置原则(dependence inversion principle) 在单一模块下包含以下含义:
* 抽象(的接口)不应该依赖(实现)细节。
* (实现)细节应依赖抽象(的接口)。
这是最小依赖原则应用在不同抽象的[模块化设计](#模块化)中使用以下公设的推论:
抽象是细节包含的子集,依赖抽象的接口较依赖实现细节具有更少的依赖。
#### 可复用性
设计应具有*可复用性(reusability)* :高层抽象设计的实现应包括复用此设计的实现的设计。
这是[最小接口原则](#最小接口原则)的推论之一,其非模式规则的输入为以下公设:
一般地,高层抽象设计和复用此设计的实现较单一的高层设计的实现更复杂。
此前提条件由对需求工作量可行性分析中的度量验证总是成立。
推论:除非必要,不分离抽象设计的实现和复用此设计的实现的设计,避免复杂性。
全局意义上的不分离设计不违反[关注点分离原则](#关注点分离原则)。
**注释**
典型实例:语言是一种高层抽象设计,语言的*库(library)* 是一种复用语言的设计。因此,语言实现应包括库设计。
另一个实例是[对象语言](../Terminology.zh-CN.md#程序设计语言)设计复用[元语言](../Terminology.zh-CN.md#程序设计语言)的[语言规则](../Terminology.zh-CN.md#规范)。
#### 可组合性
*组合(composition)* 是一种特定形式的涉及多个实体的复用,允许复用时不修改被复用的其它实体。
*可组合(composability)* 原则:接口的设计应允许不同设计之间的组合满足这些设计响应以外的需求。
这是[最小接口原则](#最小接口原则)的推论之一,其非模式规则的输入为以下过程推断得到的引理。
公设:一般地,在存在充足基础解决方案的情形下,组合现有解决方案的设计较重新给出不依赖这些解决方案的设计的解节约成本。
应用[避免不必要付出的代价](#避免不必要付出的代价),得到引理:
一般地,在存在充足基础解决方案和满足需求限制的情形下,组合现有解决方案的设计优于重新设计。
即提升可组合性可减少实现被[复用](#可复用性)的设计的成本。
### 接口设计性质和原则
#### 统一性
接口的设计应具有*统一性(uniformity)* :尽可能避免特例。
这是要求[变化的自由](#变化的自由)的推论之一,以[一致性](#一致性)作为非模式规则输入。
无限制的特例要求指定更多的附加规则避免潜在的违反一致性的风险,而违反这个要求。
因为不需要特设只有对象语言中可用的规则,[复用元语言规则](#可复用性)有利于实现统一性。
以统一的方式复用元语言和对象语言公共设施在[语法](../Terminology.zh-CN.md#计算机科学)设计上称为*光滑性(smoothness)* ,而这可推广到[语义](../Terminology.zh-CN.md#计算机科学)上(另见[正交性](#正交性)),以避免对*抽象能力(power of abstraction)* 的限制(\[Shu10] §1.1.2) 。
**原理**
在语言设计上,这类似 \[RnRK] 的设计原则 G1 :
* G1a 对象状态(object status) :语言操作[一等对象](#基本语义概念)。
* G1b 可扩展性(extensibility) :用户定义的设施能重现内建[特性](../Terminology.zh-CN.md#规范)的*能力(capability)* 。
以上原则在 NPL 中略有变化。
同 \[RnRK] ,被 G1b 重现能力的特性是内建的(built-in) 。这不同于如 \[RnRK] G2 指定的基本的(primitive) 特性。
\[RnRK] 的基本特性指不要求作为派生的(derived) ,即以[对象语言](../Terminology.zh-CN.md#程序设计语言)程序实现的特性。而内建特性适合整个[语言规范](../Terminology.zh-CN.md#规范)的接口设计约定,不论其实现是否被派生。不被要求重现的部分是实现细节。
但是,因为基本特性不要求能通过对象语言特性的组合实现,在不考虑派生特性的可实现性时,G1b 不会限定基本特性的能力。
整体上的 G1b 在和[正确性](#正确性)冲突时不被要求。这也避免了 \[RnRK] §0.1.1 指出的“妥协”。
因为语言规范不依赖使用[对象语言](../Terminology.zh-CN.md#程序设计语言)表达,G1b 仅表示用户使用语言的扩展,不表示语言自身的可扩展性;后者通过[满足需求的能力](#变化的自由)和强调支持[开放性](#开放性)体现。
仅通过[用户程序](#程序实现)实现的这种原则在 NPL 的设计中不被视为必要。但偏离这个原则的设计一般同样是不必要的。
**注释**
关于 G1a 的改变,详见[一等实体和一等对象](#一等实体和一等对象)。
#### 适用性
设计应提供*适用性(usability)* :合乎预期满足的问题领域的[特性](../Terminology.zh-CN.md#规范)。
对通用目的的领域,应进行权衡。
**注释**
这个原则存在以下的侧重不同使用方式或场景的具体表述。
结合用户的经验,这个规则的变体是之一[最小惊奇原则](#最小惊奇原则),强调降低接口的学习和适应成本。
##### 易预测性
设计应符合*易预测性(predictability)* :允许但难以偶然实现的危险操作。
同 \[RnRK] 的设计原则 G3 。
这里的危险的操作指引起较大代价的不预期或无法预期结果的操作。
这是[变化的自由](#变化的自由)和[避免不必要付出的代价](#避免不必要付出的代价)的推论,包含两方面:
* 避免危险操作的风险是[正规性](#正规性)和[避免不必要付出的代价](#避免不必要付出的代价)的推论。
* 不直接禁止危险的操作以满足上述的允许变化的要求。
避免危险的操作在许多上下文中可减少程序中易错(error-prone) 的实现的风险。
##### 可用性
一旦提供[特性](../Terminology.zh-CN.md#规范),应提供*可用性(availablity)* :保证一定程度的典型场景下能被使用。
绝大多数情形都不能使用的特性是对接口设计的一种浪费,很难符合也通常不符合[简单性](#简单性)。
可用性的概念有时也指抽象和实体具有的符合这个原则的属性。
#### 最小惊奇原则
最小惊奇原则(principle of least astonishment):在保持合理性的前提下,若能评估目标用户的接受能力,避免违反其直觉的设计。
其中,合理性至少应蕴含[正确性](#正确性),一般也蕴含[简单性](#简单性)和[适用性](#适用性)同时不违反其它原则(特别应注意尽量保持[可复用性](#可复用性)和[可组合性](#可组合性))。
这个原则主要适用于人机交互接口的设计,但也适用于一般的 [API](../Terminology.zh-CN.md#程序设计语言) 。
推论:[约定优于配置(convention over configuration)](https://zh.wikipedia.org/zh-cn/%E7%BA%A6%E5%AE%9A%E4%BC%98%E4%BA%8E%E9%85%8D%E7%BD%AE) :约定接口的合理的默认[行为](../Terminology.zh-CN.md#程序设计语言),而不是隐藏其行为而提供配置另行实现。
#### 正交性
在满足[正确性](#正确性)的前提下,接口的设计应具有*正交性(orthogonality)* :根据需求适当分解为[排除冗余和重复](#可复用性)且[能合理组合](#可组合性)的部分。
这是[最小接口原则](#最小接口原则)和[关注点分离原则](#关注点分离原则)在接口设计上的应用。
一般地,正交的设计使相同目的可使用更精简的接口组合方式实现。这也使接口具有更强的[抽象能力](#统一性)。
### 方法论
*方法论(methodology)* 是严格独立[价值判断](#价值观)的规则,是关于[价值判断](#价值观)结果参数化的判断规则。
不同的价值判断的结果作为方法论输入,决定是否适用此方法。
其它方法详见以下各节。
**注释**
一些规则因其主要表述包含价值判断而不在此归纳为方法论,尽管其中一些表述中的前提可以被参数化(如[奥卡姆剃刀](#最小接口原则)的“如无必要”的具体必要条件)。
#### 避免不成熟的优化
> Premature optimization is the root of all evil (or at least most of it) in programming.
— _The Art of Computer Programming_
原始含义适合计算机程序设计中以效率为目标的决策。
扩展的外延适用于一般需求,要求:
* 适时收缩理论长度以照顾可操作性。
* 注意断言一个优化过早自身可能就是一个过早的优化。
* 主动适应需求变更。
* 不同时明确全部的具体需求,只限定需求范围:能使用计算机实现部分语义的任务。
#### 封装
*封装(encapsulation)* 是接口设计的合理性准则。
封装是[不可分的同一性](#不可分的同一性)的一种实现方式:封装提供的接口以下的所有实现在接口从使用者的角度都是不可分的。
**注释** 若存在使用者可感知的[抽象泄漏](#避免抽象泄漏),这种实现可能失效。
以接口的预设风格的价值判断为输入,封装性要求接口满足以下*多态性(polymorhism)* :
给定接口的替代接口,则替代接口应能代替原接口,当且仅当不引起非预期的可观察的差异。
在语言设计中,去除风格参数化的这条原则被作为 [LSP(Liskov Substitution Principle)](https://zh.wikipedia.org/zh-cn/%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8D%A2%E5%8E%9F%E5%88%99) 。
参数化风格限定并非任意符合 LSP 的接口设计都符合封装性要求。这便于从不期望的设计中剔除不符合其它原则的设计。
**注释**
一些程序设计语言中的封装提供符合 LSP 的[面向对象](https://zh.wikipedia.org/zh-cn/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)风格的设施。这些设施把数据和代码组合在一起提供,但仅仅组合并不体现封装性。因此,同时具有[信息隐藏](#信息隐藏)的[特性](../Terminology.zh-CN.md#规范),如 \[ISO C++] 的类成员的访问控制的机制,被认为是典型的封装。
即便如此,封装在严格意义上和信息隐藏是相互独立的。即便语言不提供信息隐藏而仅仅指定违反封装性不关心实现细节的假设的操作[未定义](../Terminology.zh-CN.md#规范),也不失去封装性。事实上,\[ISO C++] 中,使用 `reinterpre_cast` 无视类的访问控制就是这种例子。
另一方面,LSP 事实上关于[子类型](#类型序),不限于以类作为类型的基于类的面向对象风格,实际外延更广。
#### 信息隐藏
[信息隐藏(information hiding)](https://zh.wikipedia.org/zh-cn/%E8%B3%87%E8%A8%8A%E9%9A%B1%E8%97%8F_%28%E9%9B%BB%E8%85%A6%E7%A7%91%E5%AD%B8%29) 保持不需要公开的信息不被公开,以使设计符合[最小接口原则](#最小接口原则)并支持[避免抽象泄漏](#避免抽象泄漏)。
适用于接口及其实现。
信息隐藏以是否需要公开信息的[价值判断](#价值观)(特别地,关于如何符合最小接口原则)的结果参数化。
**注释**
[封装](#封装)的接口通常有助于实现信息隐藏。直接限定避免接口规格具有过多的信息,是另一种直接的实现方式。
例如,基于类的[面向对象](https://zh.wikipedia.org/zh-cn/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)通过对[名称](../Terminology.zh-CN.md#程序设计语言)组成的表达式限制对类成员的外部访问,隐藏了类成员的信息,同时提供[封装性](#封装)。
其它方式可直接不在外部提供任何访问被封装实体的名称,如 \[RnRK] 的封装类型(encapsulate type) 和 [\[ECMAScript\] 通过 `WeakMap` 实现的封装](https://github.com/tc39/proposal-class-fields/blob/main/PRIVATE_SYNTAX_FAQ.md#how-can-you-model-encapsulation-using-weakmaps)。这些封装也同时实现了被封装实体的信息隐藏。
即便如此,如关于封装的讨论指出的,封装不一定需要实现信息隐藏。更一般地,信息隐藏的目的也不一定是提供封装。例如,系统的安全性可能直接在需求上要求隐藏特定信息,不论这种信息是否关于某种接口的实现。
#### 模块化
接口和实现的设计应具有足够的模块化(modularity) :被划分为若干保持联系的组件即*模块(module)* ,至少满足[正确性](#正确性)和[可组合性](#可组合性),并强调实现[可复用性](#可复用性)。
模块化设计通常有利于使设计具有[正交性](#正交性),但模块化相对可复用性,更侧重可组合性。
参数化的输入是需被评估模块化程度的结构设计(包括模块的*粒度(granularity)* 和组成部分的依赖关系)相对给定需求的实现质量的价值判断。
### 其它推论和比较
从对[正确性](#正确性)的强调可知,较[简单性](#简单性)优先考虑*通用性(generality)* 。
这和 \[RnRK] 中讨论的设计哲学虽然相当不同,但仍允许和 Kernel 具有相似的特性。
作为典型的 NPL 的一个[派生实现](#略称),[NPLA1](#npla1-核心语言) 具有以下和 Kernel 相似的核心设计:
* 相似的[求值](#基本语义概念)算法(差异详见 [NPLA1 求值算法](#对象语言求值算法))。
* [环境](#求值环境)可作为[一等对象](#基本语义概念)。
* 支持 [vau 抽象](#vau-抽象),且使用[词法作用域](#实现环境提供的求值环境)。
* 强调支持[对象语言](../Terminology.zh-CN.md#程序设计语言)中的[显式求值风格](#其它设计和实现参考)及[表达式](#表达式)求值前后的不同。
* 强调直接求值而非传统 LISP 方言的 [`quote`](#求值算法实现风格) 。
# 规格说明
在[附录](#附录)之前的以下章节给出 NPL 的正式规格说明的公共部分,即[语言规范](../Terminology.zh-CN.md#规范)。
本文档仅提供部分[派生实现](#略称)的规格说明。关于其它具体规格说明,详见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
在不和其它[语言规则](../Terminology.zh-CN.md#规范)冲突时,派生实现可能补充或覆盖更确切范围中生效的定义和具体语言规则。
## 略称
仅在不致混淆时使用。
* 实现(implementation) :[语言实现](../Terminology.zh-CN.md#规范)。
* 环境(environment) :[实现环境](../Terminology.zh-CN.md#规范)。
* 外部环境:(当前描述的系统边界的)外部的实现环境。
* 派生实现(derived implementation) :[派生语言实现](../Terminology.zh-CN.md#规范)。
## 补充领域定义
以下术语的定义参见[计算机体系结构](../Terminology.zh-CN.md#计算机体系结构)。
* 指令
* 指令集
* ISA
# 整体设计
一些[语言规则](../Terminology.zh-CN.md#规范)可能显式地由[派生实现](#略称)指定,或补充具体规则。
**注释**
具体讨论设计策略另见需求描述文档。
另见[设计原则](#领域设计原则)的讨论;对本章内容的描述的理解应符合其中的原则。
## 模型
可用计算机实现的语言首先是*计算的模型(model of computation)* ,或者[计算模型](../Terminology.zh-CN.md#计算机科学),对[计算](../Terminology.zh-CN.md#计算机科学)进行建模得到。
与之相关地,为计算机系统建模作为*计算机的模型(model of computer)* ,需对有限计算资源的现实进行适应。
这些模型可使用[形式方法](../Terminology.zh-CN.md#计算机科学)建立,即[形式模型](../Terminology.zh-CN.md#计算机科学)。
被计算机实现的语言应同时具有这两方面的特征。
作为实用的语言,语言还应强调提供[可编程性](../Terminology.zh-CN.md#程序设计语言)以允许用户利用;这样的语言称为*编程语言(programming language)* 。
本设计尝试在语言的原生设计中应对现有语言缺乏模型问题以避免这些妥协带来的消极影响,同时取得比非模型方法更强的可用性。
这种可用性至少体现在语义的精确性可通过模型直接决定;仅为精确性,不需要另行补充模型设计(尽管现有模型可能仍然是不完全形式化的)。
**原理**
以无限的计算资源为前提,理想的模型无法被物理地完全实现,无法直接作为计算机实现的语言的模型。
同时,这些模型仅适合对计算建模,并没有强调允许可编程性的实现;扩充可编程设计而保持模型自身的主要性质相当困难。
因此,基于计算的模型适配编程语言的设计必然需要妥协:对这些模型的裁剪和补充能提供若干编程语言的模型,但这无可避免地显著地复杂化模型自身,且不利用用户使用简单有效的规则实现通用目的上的可编程性。
事实上,使用严格形式化的模型描述编程语言的[行为](../Terminology.zh-CN.md#计算机科学)较编程语言自身的发展更落后:
* 大部分编程语言并没有使用模型支持它们的设计。
* 现实的实用语言,特别地,包括所有主流的*工业语言(industrial language)* ,几乎都没有在[语言规范](../Terminology.zh-CN.md#非自指)中给出完整的模型。
* 通常的实用语言只形式化基本的[语法](../Terminology.zh-CN.md#计算机科学)上的规则,无法指导用户精确理解程序的含义。
这些落后集中体现在的[语义](../Terminology.zh-CN.md#计算机科学)模型的缺失,使对编程语言语义的判断取决于规格说明中模型外规则的理解。
后验(postpone) 的语义模型可以使用不同[形式语义方法](#形式语义方法)设计,但和语言规范差异的一些本应避免的附加工作,并且通常难以完整地作为标准规格的描述。
**注释**
Turing 机、[无类型 λ 演算(untyped lambda calculus)](https://zh.wikipedia.org/zh-cn/%CE%9B%E6%BC%94%E7%AE%97) 等早期计算模型不考虑有限计算资源限制。
### 计算复杂度约定
特定的算法过程具有[计算复杂度](../Terminology.zh-CN.md#计算机科学)要求。除非另行指定:
* 这些复杂度是任意避免符合[错误条件](#错误)的方式调用时求值蕴含的*渐进(asymptotic)* 时间复杂度。
* 若指定边界,明确的输入规模以*哑变量(dummy)* n 表示。
* 指定复杂度的计算保证可*终止(terminate)* 。
**注释**
算法过程也适用[对象语言](../Terminology.zh-CN.md#程序设计语言)上的操作。
### 资源可用性基本约定
在[抽象机](#实现行为)的配置中,任意通过[*元语言(metalanguage)*](../Terminology.zh-CN.md#程序设计语言) 语法描述的资源总是可用的。
为避免对具体资源的总量和实现细节做出假设,除此之外,本设计只要求模型蕴含[所有权](#所有权抽象)语义(即便不严格形式化——注意作为元语言的描述模型使用的形式语言仍然可能是实现细节)。
具体计算机系统的实现中,保证基本可用的资源被直接映射到程序执行(execution) 的环境中。尽管和适配的软件环境相关,这最终由硬件实现物理地保证。
**原理**
在严格的[资源限制要求](#关注资源限制)下,[模型](#模型)不能隐藏预设的无限资源的前提。
因此,有必要做出基本的[可用性](#可用性)约定以允许表达明确的要求以避免不可实现。
## 适用领域
为尽可能解决[模型](#模型)相关的问题,优先以[通用目的](#需求概述)而不是*领域特定(domain-specific)* 语言作为评估[语言特性](../Terminology.zh-CN.md#规范)设计的参考原则。
领域特定语言的特性应能合理地从支持通用目的的特性中[派生](../Terminology.zh-CN.md#非自指),且不影响实际的可用性。
## 形式语义方法
[形式语义](../Terminology.zh-CN.md#计算机科学)方法是建立[语义模型](#模型)的形式方法。
形式语义方法主要有*公理语义(axiomatic semantics)* 、*指称语义(denotational semantic)* 和*操作语义(operational semantics)* 。
操作语义可分为在模型中指定具体[规约](#基本语义概念)步骤状态的*结构化操作语义(structural operational semantics)*(或*小步(small-step)* 语义),及仅指定规约的输入和输出的*自然语义(natural semantics)*(或*大步(big-step)* 语义)。
**注释** [抽象机](#实现行为)和[演算](#项重写系统)是使用操作语义的模型的两类例子,虽然后者也可以对[对象语言](../Terminology.zh-CN.md#程序设计语言)以外的表示建模而实现其它的语义方法。
非确定语义:经验语义,不需要使用自然语言解释的部分。
本文档不直接给出形式语义。[语言规则](../Terminology.zh-CN.md#规范)确定的经验语义可在一定条件下转写为上述形式语义方法表达的形式。
## 程序实现
[程序](../Terminology.zh-CN.md#程序设计语言)是语言的具体派生。实现程序即在语言的基础上指定具体派生规则。
[语言实现](../Terminology.zh-CN.md#规范)外的程序是*用户程序(user program)* 。
以程序或另行指定的其它形式实现的可复用程序被归类为[库](#可复用性)。
**注释** 一般地,不论是语言实现还是用户程序,都可能使用库。
除非另行指定,一个程序支持多个库的实例,之间不共享内部的状态。
语言特性包含不依赖库的*核心语言特性(core language feature)* 和*库特性(library feature)* 。
## 规范模型
NPL 是抽象的语言,没有[具体语言实现](../Terminology.zh-CN.md#规范),但一些直接影响实现表现形式的规则被本节限定。
NPL 的实现可进行*抽象解释(abstraction interpret)* ,其目标不一定是[程序](../Terminology.zh-CN.md#程序设计语言)。
任一 NPL 实现(和派生实现)的[符合性](../Terminology.zh-CN.md#非自指)由以下 NPL 符合性规则定义:文档指定的满足对实现的[要求](../Terminology.zh-CN.md#非自指)的[语言规则](../Terminology.zh-CN.md#规范)子集,包括本节、[基本文法](#基本文法)、[语义](#npl-公共语义)和其它派生实现定义的规则。
这类规则总是包含对应语言的[语义的 NPL 公共子集](#npl-公共语义),且蕴含[实现行为](#实现行为)的要求。
[语言规则](../Terminology.zh-CN.md#规范)约定的[未指定](../Terminology.zh-CN.md#程序设计语言)的程序或实现的属性及实现行为在符合性要求上等价。满足这类规则的前提下,实现选取特定的未指定的属性及对[未指定行为](../Terminology.zh-CN.md#程序设计语言)的特定实现的选择不影响实现的符合性。
**原理**
基于抽象机可直接定义最小的符合性要求,如 [C++ 的规则](https://eel.is/c++draft/intro.abstract#6)。
NPL 没有直接在此定义同等具体的规则,而以一般的要求取代。这允许派生实现对不同的具体规则[进行补充和调整](#变化的自由)。特别地,这允许[不同的方式](#形式语义方法)提供语义规则。
蕴含实现行为的要求的一个主要例子是关于[状态](#状态和行为)的规则。除了允许由实现定义和派生实现指定的不同,这实质上提供和上述具体规则等价的默认情形,而简化派生实现需要的对语言规则的补充和调整。
### 实现的执行阶段
一个 NPL 的完整实现应保证行为能符合以下的*执行阶段(phase of execution)* :
* *分析(analysis)* 阶段:处理代码,取得适当的 [IR](../Terminology.zh-CN.md#程序设计语言) 。
* *(目标)代码生成(target code generation)* :以 IR 作为输入,生成可被其它阶段执行的代码,即[目标代码(target code)](../Terminology.zh-CN.md#程序设计语言) 。
* **注释** 一般意义的代码生成可以有多个子阶段,包括多种内部 IR 的翻译,直至得到*最终目标代码(final target code)* 作为输出。
* 运行:运行生成的最终目标代码。
* **注释** 最终目标代码的形式视不同而定,可能有附加的封装格式。例如[编译器](../Terminology.zh-CN.md)的[目标代码(object code)](../Terminology.zh-CN.md#程序设计语言) 经*链接(linking)* 为可[执行](../Terminology.zh-CN.md#程序设计语言)的*映像(image)* ,被[加载](../Terminology.zh-CN.md#程序设计语言)后形式才能运行。
其中分析阶段是任意实现必要的,依次包含:
* 词法分析(lexical analysis) :必要时转换字符编码;*转义(escape)* 并提取记号。
* 语法分析(syntactic analysis) :语法检查(检验语法[正确性](#正确性))并尝试匹配记号和语法规则中的[语法元素](#语法)。
* 语义分析(semantic analysis) :语义检查(检验语义正确性)并实现其它语义规则。
以上的具体阶段不要求和实际实现中的一一对应,但应保证顺序一致。
运行之前的阶段总称为[*翻译(translation)*](../Terminology.zh-CN.md#程序设计语言) ,包含各个*翻译阶段(phase of translation)* 。
对有[宿主语言](../Terminology.zh-CN.md#规范)支持的[嵌入实现](../Terminology.zh-CN.md#规范)或目标不是程序的情况,代码生成及之后的阶段不是必须的。
宿主语言实现可提供作为[客户语言](../Terminology.zh-CN.md#规范)的 NPL 的*本机(native)* 实现。
宿主语言实现提供 NPL 实现环境,同时对 NPL 环境的操作可影响 NPL 程序,这些情形都是[元编程](../Terminology.zh-CN.md#程序设计语言),NPL 在此同时是[对象语言](../Terminology.zh-CN.md#程序设计语言)。
嵌入实现的宿主语言可直接运行语义分析的结果(中间表示)。
在语义不变的前提下,允许实现一次或多次翻译部分代码产生部分中间结果并复用。
*运行时(runtime)* 程序实现运行阶段。
其它可能的阶段由派生实现定义,但应满足所有阶段具有确定的全序关系,且不改变上述指定的阶段的顺序。符合这些条件的附加阶段称为扩展阶段。
**注释**
字符编码是被翻译的源中的二进制表示相关的模式。
### 并发实现
一个实现可能具有计算模型意义上的[并发](../Terminology.zh-CN.md#计算机科学)属性,即*并发实现(concurrent implementation)* 。
一个实现中顺序执行以上执行阶段的一组[状态](../Terminology.zh-CN.md#非自指)称为一个*执行线程(thread of execution)* ,简称*线程(thread)* 。
一个实现在整个执行过程中可以有一个或多个线程被执行。是否支持多线程执行(多线程翻译和/或多线程运行)由派生实现定义。
若实现支持多线程执行,则执行阶段的状态区分不同的并发执行线程,此时具体的状态构成由实现定义。
### 阶段不变量约束
若某些状态在某个执行阶段 k 被唯一确定为不可变状态,且在之后的状态下是*不变量(invariant)* ,则此状态称为满足 k 阶段不变量约束的。
## 正确性
*正确性(correctness)* 规则约束被执行的程序,包含语法正确性和语义正确性。
当正确性规则被发现违反时,实现进入异常执行状态。
[翻译时正确性规则](#翻译时正确性规则)以外的异常执行条件和状态由派生实现定义。
### 翻译时正确性规则
翻译时的异常状态要求给出用于区分正常状态特定的[行为](../Terminology.zh-CN.md#程序设计语言)作为[诊断](../Terminology.zh-CN.md#程序设计语言),包括[诊断消息](../Terminology.zh-CN.md#程序设计语言)和其它派生实现定义的[实现行为](#实现行为)。
语法正确性规则是翻译时正确性规则。
部分形式上的正确性规则在翻译时确保。
允许翻译时确保的形式上正确的程序是*合式的(well-formed)* ;反之*不合式(ill-formed)* 。
合式的程序符合语法和语义的正确性的规则。
其中,实现被要求确保通过翻译的程序符合语法规则和翻译时确保的*可诊断(diagnosable)* 语义规则。
不合式的程序不保证被完整地翻译,应在运行前终止执行阶段。
### 错误
*错误(error)* 是不满足预期的正确性或其它派生实现定义的不变性质时的特定诊断。
非正确性或不满足这些不变性的条件是*错误条件(error condition)* 。
满足错误条件时,实现可*引起(signal)* 错误。
**注释**
和 \[RnRS] 中的某些版本指定错误可以不诊断不同,引起错误蕴含诊断。
## 实现行为
实现的[行为](../Terminology.zh-CN.md#程序设计语言)由具有存在非特定空间上限的存储的*抽象机(abstract machine)* 描述。这种描述对应的语言的语义是*抽象机语义(abstract machine semantics)* 。
若语言规则明确特定的行为可被忽略,则被忽略之后的实现行为与之前在语言规则中视为等价。翻译的实现可选取这些等价行为中的任一具体行为。
[派生实现](#略称)可通过[显式的未指定规则](#规范模型)定义附加的等价性。
不论程序是否满足[正确性规则](#正确性),实现对程序的执行都可能存在[未定义行为](../Terminology.zh-CN.md#程序设计语言),此时实现的行为不需要满足正确性规则指定的行为要求。
特定的语言规则引入未定义行为。程序的执行在适用这些规则指定的条件时,引起未定义行为。
特定的语言规则排除未定义行为的引入,以满足一定的[可用性](#可用性)。这不排除程序的执行可能因同时使用的其它语言规则引起的未定义行为。
**注释**
抽象机语义是一种[操作语义](#形式语义方法)。
抽象机语义也可非形式地定义语言的正式的(normative) 语义和行为要求,例如 [C++ 抽象机](https://eel.is/c++draft/intro.abstract) 。
## 简单实现模型约定
### 嵌入宿主语言实现
一个派生实现使用外部语言 L 简单实现模型 NPL-EMA ,若满足:
* 以 L 为宿主语言的嵌入实现,不包含[扩展执行阶段](#实现的执行阶段)。
* 单一实现不保证提供[多线程执行](#并发实现)的支持,但对资源的使用进行适当的分组,以允许多个实现同时在宿主中多线程执行。
宿主语言提供的实现环境称为宿主实现环境,简称*宿主环境(host environment)* 。
**注释**
若支持多线程执行,需要附加的显式同步。
这种实现可能提供宿主多线程对应的实体,其中包含需要的被隔离的资源。
其它语言的实现也可能提供类似的设计,例如 [V8 的 `v8::Isolate`](https://v8.github.io/api/head/classv8_1_1Isolate.html) 。
另见[可移植](../Terminology.zh-CN.md#兼容性和可移植性)和[互操作](../Terminology.zh-CN.md#规范)意义上的[宿主环境](../Terminology.zh-CN.md#环境)。
# 基本文法
本章约定基本的 NPL [文法](../Terminology.zh-CN.md#程序设计语言)规则中,包括语法及对应的基础词法。对应的语义[在下文列出](#npl-公共语义)。
多态文法规则:[派生实现](#略称)可完全不提供本章明确定义的词法和语法构造的支持,仅当提供同构的替代文法且符合语义规则。
本章定义的[对象语言](../Terminology.zh-CN.md#程序设计语言)的正式文法以 [BNF(Backus–Naur Form ,Backus–Naur 形式)](https://zh.wikipedia.org/zh-cn/%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F)作为元语法形式地描述,且在与对象语言之间无歧义时不在终结符(terminal) 边界使用引号(即 `"` 和 `'` )。
**注释**
因为这种 BNF 使用符合 [`highlight.js` 支持的语法](https://github.com/highlightjs/highlight.js/blob/main/src/languages/bnf.js),在 Markdown 源代码中直接使用[支持的语言别名 `bnf` 标记](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md)。
## 基本文法概念
* 字符(character) :组成语言代码的最小实体。
* 基本翻译单元(basic transation unit) :作为[翻译](#实现的执行阶段)输入的任意连续字符的有限序列(可以是空序列)。
* 翻译单元(translation unit) :基本翻译单元的集合,之间满足由派生实现定义的规则。
[程序](../Terminology.zh-CN.md#程序设计语言)以翻译单元或具体操作指定的以翻译单元进行[翻译](../Terminology.zh-CN.md#程序设计语言)得到的其它变换形式表示。
## 字符集和字符串
* 字符集(character set) :对一个实现而言不变的字符的有限集合。
* 基本字符集(basic character set) :实现环境必须支持的字符集。具体由派生实现定义。
* 字符串(character string) :字符集上的序列。
除非另行指定,关于字符集定义的其它概念同 \[ISO C++11] 对 character 和 character set 的有关定义。
**注释**
[字符编码](#实现的执行阶段)基于字符集定义。
一般地,一个翻译单元只具有同一个字符编码。
## 词法规则
*词法规则(lexical rules)* 约定在字符基础上的最小一级的可组合为[语法](#语法)元素单位直接关联的文法规则。
约定[元语言](../Terminology.zh-CN.md#程序设计语言)语法 `` 表示词法元素 `x` ,`::=` 表示定义,`|` 表示析取。
### 基本词法构造
文法:
```bnf
::= | <$punctuator> | <$identifier>
```
* 分隔符(delimiter) :代码中标记特定字符序列模式的字符序列。
* 词素(lexeme) :代码中以分隔符确定边界的字符序列。
* 记号(token) :词素的顶级分类。
属于记号的语法元素可以是以下的词法分类:
* 字面量(literal) :一种记号,参见[以下描述](#字面量)。
* 标点(punctuator) :由派生实现定义的特定字符序列的集合,可具有一定语义功能。
* 标识符(identifier) :除字面量和标点以外的记号
代码中邻接的分隔符和非分隔符不构成一个词素。
不在记号内包含的空白符是分隔符,而不是词素。
标点是分隔符,也是词素。
超过一个字符的标点可能在匹配字符序列确定是否构成词素时具有词法歧义。此时,应指定消歧义规则确保存在唯一可接受的匹配方式,或引起词法[错误](#错误)终止翻译。
除非派生实现指定,字面量以外的记号不包含分隔符。
记号是可能附带附加[词法分析](#词法分析)信息的词素。词法分析后得到的记号可以用词素映射到词法分类的有序对表示,但 NPL 不要求在此阶段保持分类也不限定表示的构造。
可以保证 \[ISO C++11] 的 identifier 的定义,或在上述标识符中插入字符 `$` 构造得到的标识符属于 NPL 标识符。
派生实现可定义其它能构成标识符的词素。
**注释**
NPL 不指定超过一个字符的分隔符,因此默认没有词法歧义。派生实现可指定这些规则。
NPL 是*自由形式(free form)* 的语言,空白符原则上不构成字面量以外的词素和语义。
### 转义序列和字符序列
文法:
```bnf
::= <$single-escape-char> | <$escape-prefix-char><$escape-content-seq>
::= <$literal-char> |
```
包含 `` 的 `` 包括:
* `\'`
* `\"`
* `\\`
* `\a`
* `\b`
* `\f`
* `\n`
* `\r`
* `\t`
* `\v`
`` 的含义同 \[ISO C++] 的对应转义序列。
**注释** 这是 \[ISO C++] 的 `` 词法分类中除了 `"\?"` 的情形,也是 \[R6RS] 在 `` 中支持的字面情形。
### 字面量
文法:
```bnf
::= |
::= ''
::= ""
::= |
::= | <$derived-impldef-literal>
```
* 代码字面量(code literal) :以 ' 作为起始和结束字符的记号。
* 数据字面量(data literal) :以 " 作为起始和结束字符的记号。
* 字符串字面量(string literal) :代码字面量或数据字面量。
* 扩展字面量(extended literal) :由派生实现定义的非代码字面量或数据字面量的记号。
* 字面量(literal) :代码字面量、数据字面量、字符串字面量或扩展字面量。
派生实现定义的解释可排除代码字面量作为字符串字面量。
**原理**
传统的字面量一般是[自求值项](#范式),这包括一般的字符串字面量。
代码字面量可提供非自求值项的处理方式。
### 分隔符
以下单字符标点是 NPL 图形分隔符:
* `(`
* `)`
* `,`
* `;`
以下单字符标点是 NPL 分隔符:
* NPL 图形分隔符
* 空白符(字符串 " \n\r\t\v" 中的字符之一)
**注释**
空白符同 \[ISO C++] `std::isspace` 在 C 区域下的定义,不含`空字符(null character)` 。
**原理**
NPL 图形分隔符可不和其它字符组合而作为单独的记号。因此,这不包含构成字面量的字符 `'` 和字符 `"` 。
NPL 分隔符用于一般分隔记号(而不是识别[字面量](#字面量))的外部描述,也没有显式地包含这些字符,但词法分析仍应把按字面量规则把这些字符作为必要时区分不同记号的边界。
### 词法分析
[词法分析](#实现的执行阶段)输入[翻译单元](#基本文法概念),输出记号序列。
以下规则(按优先顺序)定义了词法分析转换输入为输出的步骤:
* 反斜杠转义:连续两个反斜杠被替换为一个反斜杠。
* 引号转义:反斜杠之后紧接单引号或双引号时,反斜杠会被删除。
* 断行连接:反斜杠之后紧接换行符的双字符序列视为续行符,被删除使分隔的行组成逻辑行。
* 字面量:未被转义的单引号或双引号后进入字面量解析状态,无视以下规则,直接逐字节输出原始输入,直至遇到对应的另一个引号。
* 窄字符空白符替换:单字节空格、水平/垂直制表符、换行符被替换为单一空格;回车符会被忽略。
* 原始输出:其它字符序列逐字节输出。
不对空字符特殊处理。
**注释**
因为不一定是 [NPL 分隔符](#分隔符),转义字符不总是分隔标识符。
## 语法
本节指定 NPL 作为对象语言的[语法](../Terminology.zh-CN.md#程序设计语言)。
约定[元语言](../Terminology.zh-CN.md#程序设计语言)语法 `` 表示语法元素 `x` ,`::=` 表示定义,`|` 表示析取。
程序被作为[语言实现](../Terminology.zh-CN.md#规范)组成部分的[语法分析](#实现的执行阶段)程序[规约](#基本语义概念),结果能确定其和一定的语法元素匹配。
规约时应进行语法规则的检查。
### 基本语法构造
NPL 的基本语法单元是可递归构造的[表达式](#表达式),或派生实现指定的其它语法构造。
构成基本语法单元的规则参见[词法规则](#词法规则)。
[合式](#翻译时正确性规则)的[基本翻译单元](#基本文法概念)应是一个或多个基本语法单元。
### 表达式
文法:
```bnf
::= | |
```
*表达式(expression)* 是受表达式语法约束的记号序列,可以是:
* *[原子表达式(atom expression)](#原子表达式)*
* *[复合表达式(composite expression)](#复合表达式)*
* *[列表表达式(list expression)](#列表表达式)*
构成表达式的表达式是被构成的表达式的*子表达式(subexpression)* 。
#### 原子表达式
文法:
```bnf
::=
```
原子表达式不能被表示为其它表达式的语法构成形式的复合。
#### 复合表达式
文法:
```bnf
::= |
```
复合表达式是原子表达式和表达式的复合,即语法意义上的直接*并置连接(juxtaposition)* ,不在被复合的表达式之间存在其它记号。
同一个表达式可能被按原子表达式出现的位置以不同的方式规约为复合表达式。允许的规约复合表达式的方式由派生实现定义。
#### 列表表达式
文法:
```bnf
::= |
::= | |
::=
::= ( |
::= ) |
```
其中,`` 是 [NPL 分隔符](#分隔符)。
列表表达式是在其他表达式的序列(可能为空)左右附加一组 `` 和 `` 作为边界构成的表达式。
`` 和 `` 是不同的标点。
边界为 `(` 和 `)` 的表达式是基本列表表达式。其它可能的边界由派生实现定义,构成扩展列表表达式。
**注释**
对 `` 的定义,以[形式语义使用的元语言](#npl-公共语义) 中的元语法符号 `*` 扩展元语法,可以简记作 ` ::= * ` 。
[列表表达式](#列表表达式)的边界是 [NPL 图形分隔符](#分隔符)。
### 名称
NPL 的*名称(name)* 是符合语法规则约束的若干[记号](#基本词法构造)的集合。
存在非空的名称集合可被作为[表达式](#表达式)。
**原理**
[名称](../Terminology.zh-CN.md#程序设计语言)的集合是[广义实体](../Terminology.zh-CN.md#程序设计语言)和[实体](../Terminology.zh-CN.md#程序设计语言)的差集。
语言规则对语言可表达的名称添加要求,以使语言的[源代码](../Terminology.zh-CN.md#程序设计语言)能够直接使用名称。
名称在源代码形式之外也可广泛存在,且能通过不唯一的方式构造。因此,语言规则允许不和源代码形式一一对应的名称。
**注释**
构成名称的集合的表现形式不唯一。
特定的名称可能为空集。
约束通常包含顺序,即其中的记号构成确定顺序的序列。
记号或记号集合经[编码](../Terminology.zh-CN.md#程序设计语言),一般可实现为可表达的字符串。
### 语法形式
*语法形式(syntactic form)* 是词法上满足特定[形式](../Terminology.zh-CN.md#非自指)的语法构造。
除非派生实现另行指定,语法形式总是表达式。
### 语句
以派生实现定义的标点结尾的表达式称为*语句(statement)* 。
语句语法的*分组(grouping)* 规则以及是否隐式地作为列表表达式[求值](#基本语义概念)由派生实现定义。
### 简单文法约定
一个派生实现使用简单文法 NPL-GA ,若满足:
* 翻译单元同[基本翻译单元](#基本文法概念)。
* 只支持左原子表达式构成[复合表达式](#复合表达式)。
* 只支持基本[列表表达式](#列表表达式)。
* 标点为单个[字符](#基本文法概念)。
* 若支持语句,总是 NPL-GA [表达式](#表达式)。
**原理**
NPL-GA 允许一些典型的*分析器(parser)* 简化设计作为实现。
在[表达式](#表达式)的形式文法仅作为语法规则,使用[词法分析](#词法分析)的结果提供作为*语法类别(syntactic category)* 的[词素](#基本词法构造)的[串](../Terminology.zh-CN.md#程序设计语言)作为输入的情况下,NPL-GA 支持 LL(1) 文法分析,即使用 NPL-GA 语法。
若延迟[复合表达式](#复合表达式)和[列表表达式](#列表表达式)中的选择到分析器外(之后可能由语义处理),检查语法的判定程序可进一步简化,仅判断记号 `(` 和 `)` 的匹配。
若词法分析处理直接对 `(` 和 `)` 和进行*记号化(tokenize)* 标记,则 NPL-GA 分析器不需要支持其它判定。这样的分析器实现的 NPL-GA 子集等效 LL(0) 文法。但由于 NPL-GA 不限定语法元素具体数量,等效 LL(0) 分析器当且仅当输入的串终止时接受输入,因此是*平凡的(trivial)* ,通常不具有实际意义,因为:
* 形式上这里只有算法步骤的多少的差异,而几乎所有实现的语言都不把它作为[可观察行为](#状态和行为)。
* 即便需要统计串的长度,也应可以在之前(词法分析)计算,使用语法分析完成这个任务在此是低效的。
反之,在分析 NPL-GA 语法前扩展其它语法*预处理(preprocessing)* 规则可以支持更多的文法扩展。这样的文法扩展可接受扩展的非 NPL-GA 文法,但仍允许保持语法分析器的实现使用 NPL-GA 语法。
# NPL 公共语义
NPL 的[语义规则](../Terminology.zh-CN.md#程序设计语言)构成[*演绎系统(deductive system)* (en-US)](https://en.wikipedia.org/wiki/Formal_system#Deductive_system) ,通过对[翻译单元](#基本文法概念)中的[表达式](#表达式)的[求值](#基本语义概念)表达。
除非[派生实现](#略称)另行指定,仅使用表达式指定关于[对象语言](../Terminology.zh-CN.md#程序设计语言)中的计算的语义。
基本语义规则要求:
* 所有不需要[诊断消息](../Terminology.zh-CN.md#程序设计语言)的规则由派生实现定义。
* 本章内的规则应不引入[未定义行为](../Terminology.zh-CN.md#程序设计语言)。
NPL 允许程序具有语义等价的[未指定行为](../Terminology.zh-CN.md#程序设计语言)。派生实现可能通过约定和限制其具体选项的选取以指定更具体的[实现行为](#实现行为)。
本章内的语言语义基于一种[EBNF(扩展 BNF )](https://zh.wikipedia.org/zh-cn/%E6%89%A9%E5%B1%95%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F) 的方言,其中:
* 规则定义符号(defining symbol) 为 `::=` 。
* 除非元素的含义和用法被单独另行指定,作为文法类的元素使用 `<` 和 `>` 作为分隔符。
* **注释** 这和 BNF 相同,但和常见 EBNF 可能不同。
* 在和对象语言之间没有歧义的情形时:
* 使用 `*` 作为修饰前一个元素的 [Kleene 星号](https://zh.wikipedia.org/zh-cn/%E5%85%8B%E8%8E%B1%E5%B0%BC%E6%98%9F%E5%8F%B7),表示之前修饰的项可出现 0 次或多次。
* 使用 `(` 和 `)` 组合之间的元素。
* 若对象语言直接出现元语言的元符号(metasymbol) 而出现歧义,需要用一对 `"` 或 `'` 标记。
* **注释** 此时和非原始版本的 BNF 以及常见 EBNF 可能相同。
**原理**
[ISO/IEC 14977:1996](https://www.cl.cam.ac.uk/~mgk25/iso-14977.pdf)正式定义了最常见的方言,但这一标准没有简化业已分歧的元语法方言,以至于[仅在 ISO 的不同编程语言标准中,就存在多种记法并用](https://www.grammarware.net/text/2012/bnf-was-here.pdf)。
BNF 作为元语法形式地描述适用描述形式语言的[串](../Terminology.zh-CN.md#程序设计语言)的[重写系统](#演绎规则),而非对描述语义更灵活的[项重写系统](#项重写系统)。
在语法之外,元素之间的空白符通常不是重要的。因此,可直接省略元语言分隔符 `<` 和 `>` 。这也是大多数 EBNF 的典型用法(即便描述的是语法)。描述语义的例子如 \[Shu10] ,其中使用特定的字体支持元语言中出现的元素。
然而,为便于在代码中表示而不依赖特设的字体,此处仍然使用 `<` 和 `>` ,除非元素的含义和用法被单独另行指定。
**注释**
因为这种 EBNF 使用不符合 [`highlight.js` 支持的语法](https://github.com/highlightjs/highlight.js/blob/main/src/languages/ebnf.js),在 Markdown 源代码中不使用[支持的语言别名 `ebnf` 标记](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md),而记作自定义语言标记 `xbnf` 。
ISO EBNF 被 `highlight.js` 的 `ebnf` 语法定义支持。
\[Shu10] 也使用 `::=` 作为规则定义符号。
## 基本语义概念
* 区域(region) :和特定位置代码关联的有限[实体](../Terminology.zh-CN.md#程序设计语言)集合。
* 范围(range) :一个连续区间。
* 此处“连续”的概念由派生实现定义,默认参照数学的形式定义。
* 声明(declaration) :引入单一[名称](#名称)的表达式。
* 声明区域(declarative region) :对某一个声明及其引入的名称,通过[声明区域规则](#声明区域约定)决定的范围。
* 有效名称(valid name) :可以唯一确定[指称](../Terminology.zh-CN.md#程序设计语言)的实体的名称。
* 有效命名实体(valid named entity) :有效名称指称的实体。
* 名称隐藏(name hiding) :若同一个名称在同一个位置属于超过一个声明区域,则应能通过[名称隐藏规则](#可见名称)确定唯一有效的声明以指定有效名称和对应的有效命名实体,此时有效名称隐藏其它声明区域声明的名称,有效命名实体隐藏可以使用被隐藏名称指称的实体。
* 作用域(scope) :声明区域的子集,满足其中指定的名称是有效名称。
* 生存期(lifetime) :逻辑上关于实体的[可用性](#可用性)的连续区间的抽象,是一个闭集。
* 属性(property) :实体表现的性质。
* 同一性(identity) :实体上的一种[等价关系](../Terminology.zh-CN.md#计算机科学),允许实体具有标识不相等特定的属性。
* **注释** 特定属性的例子如占据存储。
* 对象(object) :可确定同一性的实体。
* 值(value) :表达式关联的[不可变状态](../Terminology.zh-CN.md#非自指)。
* 作为实体,对象总是关联值作为它的内容,称为*对象的值(value of object)* 。
* 未指定值(unspecified value) :[未指定](../Terminology.zh-CN.md#程序设计语言)的值。
* 修改(modification) :使[状态](../Terminology.zh-CN.md#非自指)被[改变](../Terminology.zh-CN.md#非自指)的操作。
* 作用(effect) :语言支持的一定上下文内的表达式规约蕴含的[计算作用](../Terminology.zh-CN.md#程序设计语言)。
* 副作用(side effect) :对表达式的值以外的表示的改变的作用。
* 幂等性(idempotence) :重复后即不改变状态的性质。
* 项(term) :特定的演绎系统中处理的对象,是带有基本递归构造的元素,可对应语法中的表达式。
* **注释** 这样的演绎系统主要是[项重写系统](#项重写系统)。
* 子项(subterm) :具有递归形式构造的文法描述的参与构成项的项。
* 变量(variable) :通过声明显式引入或通过演绎系统规则隐式引入的以名称指称的实体。
* 绑定(binding) :引入变量的操作或结果,其中后者是变量的名称和引入的被变量表示的实体构成的有序对。
* 约束变量(bound variable) :子项中出现的名称被绑定的变量,即其指称可能依赖具体上下文的变量。
* 同名的约束变量的整体重命名替换不保证不改变指称进而可能影响语义。
* 自由变量(free variable) :子项中出现的非约束变量。
* 组合子(combinator) :不是变量也不含相对任何项的自由变量作为子项的项。
* 常量(constant) :满足某种不变量的约束以和不可变状态关联的实体。具体由派生实现定义。
* **注释** 不和变量对立:蕴含不可变状态的变量可能是常量。
* 转换(conversion) :根据基于特定等价性(假设)前提的两个项之间的自反的演绎。
* 规约(reduction) :两个项之间的、[实例](../Terminology.zh-CN.md#程序设计语言)是某个转换的子集的满足反自反的演绎。
* 抽象求值(abstract evaluation) :对表达式的不取得作用的规约。
* 具体求值(concrete evaluation) :对表达式的取得作用的规约。
* 求值(evaluation) :抽象求值或具体求值。
* **注释** 即对表达式的规约。
* 求值结果(evaluation result) :作用的子集,是求值得到的用于替换被求值的表达式作为它的值的实体,或其它由派生实现定义的实体。
* 不和其它结果混淆时,简称*结果(result)* 。
* 求值中取得求值结果中的表达式的值的过程称为*值计算(value computation)* 。
* 值计算包含确定用于替换的实体以及替换的过程,两者之间具有因果性。
* 值对象(value object) :表示值的对象。
* **注释** 值对象是可作为值使用的对象,例如作为求值结果的一部分。和值不同,值对象不一定是不可变状态。
* 控制状态(control state) :实现中决定求值的状态。
* 程序表现的控制状态通称*控制(control)* 。
* 特定控制状态的改变使不同的实体被求值,这对应控制*转移(transfer)* 。
* *调度(schedule)* 特定可能改变控制作用的实体可决定如何转移控制状态。
* **注释** 这可能实现[并发的](../Terminology.zh-CN.md#计算机科学)计算。
* 除非派生实现另行指定,控制状态是[区分多线程执行中不同线程的状态](#并发实现)。
* 控制作用(control effect) :引起控制状态改变的作用。
* 在 NPL 中,控制作用是在对象或派生实现定义的实体上引起改变的副作用。
* 相等关系(equality relationship) :定义在值的集合上的等价关系。
* 布尔值(boolean value):逻辑真或逻辑假。
* 谓词(predicate) :若具有结果,则结果是布尔值的实体。
* 数据结构(data structure) :[数据](../Terminology.zh-CN.md#程序设计语言)的构造性表示。
* 一等实体(first-class entity) :语言表达的允许支持足够特性的子集的实体,其中特性支持包括:
* 可作为语言中的有效命名实体。
* 可作为语言中的值在[特定的对象语言构造](#λ-完备语义和对应语法)中使用。
* 可表达数据结构。
* 满足以上支持的值域中没有任意特设的限制。
* **注释** 使用的判定准则和 \[RnRK] Appendix B 的 first-class object 的约定实质上一致。
* 一等对象(first-class object) :可确定同一性的一等实体。
* 访问(access) :从实体上取得状态或修改实体。
**原理**
一些设计中,值对象是专用于[表示(不可变的)值的对象(en-US)](https://en.wikipedia.org/wiki/Value_object) 。本设计不使用这个定义,因为:
* 值对象作为对象,蕴含[表示](#表示)的目的,在语言设计而非实现的上下文中不是值的等义词。
* 以实现角度考察值对象提供值的表示时,不关心它[是否可作为一等对象](#可变状态和普遍性)而要求[不可变](#实体的不可变性)可允许其上的[副作用](#基本语义概念)替换表示具有其它的值。
* 作为对象的值,它可能因为[互操作](../Terminology.zh-CN.md#规范)等目的在外部被直接作为其它语言实现中可作为(允许可变的)一等对象的实体。
**注释**
在实现执行的上下文,生存期概念兼容 [ISO/IEC 2382](https://www.iso.org/standard/63598.html) 的 lifetime 定义:
> portion of the execution duration during which a language construct exists
定义绑定的有序对作为抽象表示,不需要被对象语言支持。对象语言可支持其它具体的有序对数据结构。
典型地,作用包括计算得到的值、引起的副作用以及其它可由区域和变化的状态二元组描述的实体。
一等对象同时是对象。
为满足可在表达式中通过求值被使用,一等实体总是能关联表达求值结果的值,称为实体的值。
### 表示
[表示](../Terminology.zh-CN.md#程序设计语言)用于表现演绎实例、具体实现及其中一部分实体的状态。
**注释** 其中的一部分实体可以是某个值。
因为保证同一性,对象的值作为[可变状态](#状态和行为)的表示时,即对象存储的值。
**注释** 变量不一定是可变状态的表示。
[外部表示](../Terminology.zh-CN.md#程序设计语言)和[内部表示](../Terminology.zh-CN.md#程序设计语言)是相对的。不同[外部环境](#略称)可以有不同的外部表示,这些外部表示相对其它外部环境而言可以不是外部表示。
外部表示可能被*读取(read)* 处理为内部表示。内部表示可能被*写入(write)* 处理为外部表示。
读取和写入操作的副作用分别是*输入(input)* 和*输出(output)* 。
外部表示为元素序列时,读取和写入是非特定格式数据和元素序列之间的转换,若不含其它[作用](#基本语义概念),其操作是进行*反序列化(deserialize)* 和*序列化(serialize)* 。
内部表示为对象时,读取和写入包含对象和非特定格式数据之间的转换,其操作是进行*列集(marshall)* 和*散集(unmarshall)* 。
除非另行指定,不要求对象语言提供内部表示到外部表示的转换。
[文法](#基本文法)约定基准的表示作为[翻译](../Terminology.zh-CN.md#程序设计语言)的输入。这种表示是翻译所在外部环境的外部表示,即[源代码](../Terminology.zh-CN.md#程序设计语言);翻译结果是对象语言代码,简称*对象代码(object code)* ,可以是另外的外部表示。
[翻译单元](#基本文法概念)是这里被翻译的外部表示。
由[基本文法](#基本文法),空白符和参与的表示,不一一对应。为便于输出标准化,NPL 约定以下*规范(canonical)* 外部表示:
* 对列表,输出的表示是以 `(` 和 `)` 作为边界,元素以单个 ` ` 为分隔符的序列,其中的元素在括号中被递归地嵌套表示。
* 对非列表的存在唯一的对应词法形式(如字面量)的值,输出这个值的词法形式。
* 其它值的外部表示[未指定](../Terminology.zh-CN.md#程序设计语言)。
* **注释** 值自身可能[未指定](../Terminology.zh-CN.md#程序设计语言)。[未指定值](#基本语义概念)通常蕴含未指定的外部表示,但不绝对。
谓词在模型中表示为数学关系、映射或单值函数;在对象语言中可有不同的表示,如[函数](#函数)。
其它外部表示和内部表示的外延由派生实现定义。
#### 同像性
外部表示和内部表示可能部分共享相同的规则。这些表示是*同像的(homoiconic)* 。语言支持同像的表示及其有关特性的性质是[*同像性(homoiconicity)*](https://zh.wikipedia.org/zh-cn/%E5%90%8C%E5%83%8F%E6%80%A7) 。
典型地,同像性允许复用代码和数据之间的表示。特别地,同像性允许对象语言中的代码作为数据(code as data) ,而不需要显式地处理为和代码不同的数据结构,显著简化[元编程](../Terminology.zh-CN.md#程序设计语言)的接口复杂性。
除非另行指定,NPL 和派生实现不限制语言中任何不同表示之间可能具有的同像性。
**原理**
[存储程序的体系结构](https://zh.wikipedia.org/zh-cn/%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC%E7%BB%93%E6%9E%84)自然而普遍地依赖代码和数据具有相同的表示,以便有效地把存储的数据直接作为代码提供给控制部件。
存储程序型计算机因此能自然地支持[自修改代码](https://zh.wikipedia.org/zh-cn/%E8%87%AA%E4%BF%AE%E6%94%B9%E4%BB%A3%E7%A0%81)。在更高层次的抽象中,高级语言可能改变这些性质的可用性,使其符合[最小接口原则](#最小接口原则),符合安全设计的需要。
但是,自修改程序在一些情形下仍然必要。为了[通用目的](#变化的自由),这些设计应符合[易预测性](#易预测性),而非完全禁止。
通过语言规则指定的同像性不具有体系结构设计时依赖的具体数据表示容易引起非预期操作的风险,有必要作为公开特性。
不需对代码和数据分别提供不同的特性有利于语言设计和使用的[简单性](#简单性)。
这同时使对象语言不需要提供特设的对自身的*反射(reflection)* 特性,因为潜在可被反射的对象伴随一般的元编程无处不在而可被随时引入或排除,直至另行指定的规则限制这种能力。
这也是使对象语言的设计符合[光滑性](#统一性)的主要机制。
**注释**
自修改程序在一般意义下对运行时生成代码的 JIT(just-in-time) [编译器](../Terminology.zh-CN.md) 是必要的。这有助于提升程序运行时性能。
另行指定的规则包含显式的涉及表示的转换规则,例如[语法分析等阶段](#实现的执行阶段)可能转换外部表示为不同的(不确定种类的)内部表示,这些表示不保证其中的组成在变换前后一一对应。但是,NPL 规则没有明确指定破坏可能具有对应的规则,因此不同内部表示之间的非同像性仅在派生实现中可能指定。
### 演绎规则
演绎系统具有的演绎规则决定演绎推理(deductive reasoning) 的输出。
指定转换输入和输出之间的关系的演绎规则是转换规则。
两两可转换的对象的传递闭包构成[等价类](../Terminology.zh-CN.md#计算机科学),称为可转换等价类。除非另行指定,以下只讨论具有单一可转换等价类的转换规则演绎系统,即(抽象)*重写系统(rewriting system)* 。
对象之间的转换保持某种[等价关系](../Terminology.zh-CN.md#计算机科学)的等价*变换(transformation)* 。对象之间的规约是其中的子集,即以存在等价关系的一个对象替代另一个对象的有向转换。
若两个对象具有规约到相同结果的变换,这两个对象*可连接的(joinable)* 。
若任意两个对象等价蕴含对象可连接,则此重写系统具有 *Church–Rosser 属性(Church–Rosser property)* 。
若可从任意一个对象规约到的任意两个对象可连接,则重写系统具有*汇聚性(confluence)* 。
若可从任意一个对象的一步规约到的任意两个对象可连接,则重写系统具有*局部汇聚性(local confluence)* ,或称为*弱汇聚性(weak confluence)* 。
若可从一个对象规约到的任意两个对象可连接,则此对象具有汇聚性。
若可从一个对象的一步规约到的任意两个对象可连接,则此对象具有局部汇聚性,或称为弱汇聚性。
规约中可包括涉及[实现环境](../Terminology.zh-CN.md#规范)的交互。
若规约用于求值,汇聚性限定为:满足任意以此规则变换前和变换后的项被分别规约时,两者的[作用](#基本语义概念)相等。
### 项重写系统
作为[重写系统](#演绎规则)的实例,一个[*项重写系统(term rewriting system)*](https://en.wikipedia.org/wiki/Rewriting#Term_rewriting_systems) 包含以下组成:
* 语法元素(syntactic element) 的集合。
* 项及其子项是语法元素的非空的串。
* 辅助语义函数(auxiliary semantic function) 的集合。
* 可通过语义变量(semantic variable) 指称其中的元素。
* *重写规则(rewrite rule)* 的集合。
* 重写规则指定*重写(rewrite)* :接收项输入并产生作为重写的输入的项,和被重写的项之间满足某种[等价关系](../Terminology.zh-CN.md#非自指)即*重写关系(rewrite relation)* 。
* 重写规则集合以包含语法元素和语义变量的重写关系在[元语言](../Terminology.zh-CN.md#程序设计语言)中表达为*模式(scheme)* 。
语法上蕴含[自由变量](#基本语义概念)的项是*开项(open term)* 。*闭项(closed term)* 是开项在项上的补集。
为表达计算,限制特定的重写关系使之不满足自反性,得到*规约关系(reduction relation)* ,即指定[规约](#基本语义概念)。对应地,双向的重写规则限制为其子集的单向的*规约规则(reduction rule)* 。经限制的系统是*项规约系统(term reduction system)* 。
规约关系视为表达计算查询(computational query) 的项和答案(answer) 的项之间的映射。此时,项规约系统被作为一种[计算模型](#模型)。
* **注释** 为表达计算的答案的确定性,需要确保规约可能取得[范式](#范式)。
一般地,项规约系统关联的结构总称为演算(calculus) 。
对每个演算,存在和项对应的*上下文(context)* 。元语言中,一般的上下文以语义变量 `C` 表示,形式化为具有元变量(meta variable) □ 的以下构造:
```xbnf
C ::= □ | ...
```
其中 `...` 是演算支持的项的语法中替换[子项](#基本语义概念)得到的对应构造。
**注释** 例如,对作为对象语言的 λ 演算,语义变量 `x` 表示约束变量,其上下文为:`C ::= □ | (CT) | (TC) | (λx.C) 。这里的记法基本同 \[Shu10] 。
一般的项记作语义变量 `T` ,则 `C[T]` 表示上下文 `C` 中作为元变量通过语法代换(syntactic replacement) 为项 `T` 的结果,它是一个项。
作为对象语言的[变量](#基本语义概念)的项可依赖不同的上下文指称不同的实体。
一个变量 `x` 被上下文 `C` *捕获(capture)* ,若对任意 `x` 是其[自由变量](#基本语义概念)的项 `T` ,`T` 中自由出现的 `x` 在 `C[T]` 中是[约束变量](#基本语义概念)。
**注释** `C` 中仍可因自由出现的 `x` 而使 `x` 是 `C[T]` 中的自由变量。
### 状态和行为
状态不变蕴含语言规则中或可选地由实现定义的[等价关系](../Terminology.zh-CN.md#非自指)决定。
除非派生实现另行指定,约定:
* [实现行为](#实现行为)总是可使用状态进行描述。
* 存在[副作用](#基本语义概念)为*可观察(observable)* 行为的必要条件。
* 在实现外部[访问](#基本语义概念)某个状态的操作(输入/输出操作)是副作用。
若存在状态等价性以外描述的行为描述,由派生实现指定。
可观察行为如有其它外延,由派生实现指定;否则存在副作用是存在可观察行为的充分条件。
实现应满足实现行为和语义蕴含的可观察行为等价。除派生实现指定的更特定的具体行为等价性外,其余的行为等价性[未指定](../Terminology.zh-CN.md#程序设计语言)。
实现可支持实体具有对外部不引起可观察行为差异的*隐藏状态(hidden state)* 。
隐藏状态和程序约定的一些状态作为*管理状态(administrative state)* ,以隐藏局部的状态变化对程序中其它状态的影响。
非管理状态是*数据状态(data state)* 。
**原理**
形式上,可观察的性质影响特定的项的*操作等价性(operational equivalence)* :替换操作等价的项得到的两个规约在可观察性质上是等价的,即两个规约的结果相等(对应行为不可分辨)。因此,可观察的性质可形式化为作为这些等价规约的结果的参数。
最简单的做法,如 \[Shu10] §8.3.2 把具有可观察性质的项处理为常量语法域(syntactic domain) ,不需要附加定义相等性或影响其它规约规则。
对语义蕴含的可观察行为等价的要求指定了允许实现进行*语义保持变换(semantic preserving transformation)* 不能修改可观察性质的内涵,进而明确了实现对程序的可优化的界限。
数据状态和管理状态的分类类似 \[RnRK] 中改变对象的性质上对状态的划分,但不仅仅应用在关于改变对象的判断上。
改变对象意义上和 \[RnRK] 对应的具体实例是[实体的不可变性](#实体的不可变性)。
**注释**
关于实体的状态,参见[实体的等价性](#实体的等价性)。
不严格要求实现行为和[抽象机语义](#实现行为)蕴含的所有推论一致。
NPL 派生实现不保证是纯函数式语言,其中的[计算](../Terminology.zh-CN.md#计算机科学)允许描述状态的[改变](#基本语义概念)。表达式的求值的[作用](#基本语义概念)和 \[ISO C] 以及 \[ISO C++] 类似。不同的是,本文档的定义明确指定[控制作用](#基本语义概念)的更一般外延:改变[控制状态](#基本语义概念),即便这些状态并非从属[一等实体](#基本语义概念)。特别地,最简单的条件分支也明确具有副作用。
### 作用使用原则
派生实现可定义其它的作用。
在推理[实现行为](#实现行为)时,副作用应仅在必要时引入。
作用具有*种类(kind)* 。[值计算](#基本语义概念)是作用的种类。
[副作用](#基本语义概念)中,对象的改变是一种作用的种类。
是否存在副作用是互斥的,即一种作用不可能同时是副作用和不是副作用。其它作用的种类可能相交,即可能同属不同的作用。
派生实现可定义其它作用的种类。
求值可引起副作用的*起始(initiation)* 。副作用的存在(如改变状态)可继续保持到求值结束后,并可影响[可观察行为](#状态和行为)。
副作用的*完成(completed)* 即副作用的存在的终止(如改变状态完成)。
引起作用的求值*蕴含(imply)* 求值关联的作用,以及其中蕴含的副作用的起始决定的其它作用。派生实现可定义特定的求值使之蕴含的其它的作用。
作用之间可存在[等价关系](../Terminology.zh-CN.md#计算机科学)。等价的作用相互替换不影响可观察行为。
**原理**
允许派生实现定义不同的作用以维护[变化的自由](#变化的自由)。
不同[副作用](#基本语义概念)对[行为](../Terminology.zh-CN.md#程序设计语言)的影响可能依赖作用之间的顺序。
因此,副作用应仅在必要时引入,不能在推理行为时无中生有(out of thin air) ,除非证明引入的副作用不蕴含[被许可的等价的实现行为](#实现行为)以外的其它行为。通常需明确区分是否依赖副作用以避免非预期的行为。这有助于保持[易预测性](#易预测性)和[可组合性](#可组合性)。
NPL 及其派生实现中的作用可描述一般的[计算作用](../Terminology.zh-CN.md#程序设计语言),不限定作用的种类的外延。
明确副作用的起始是必要的,因为语言至少需要支持允许无法反馈外部状态完成改变的副作用,即 I/O 操作,此时副作用的存在应被允许保持到求值结束后,否则求值无法终止而被*阻塞(blocked)* 。
副作用的完成是和起始相对的概念,在讨论有关[顺序](#规约顺序)时可能实用。
**注释**
派生实现可定义的其它的作用可能是副作用。
副作用的起始在 \[ISO C++] 的关于求值(引起的作用)的规则中同样被明确。
## 实体语义
实体是语言中主要表达的目标。
本节提供和实体相关的公共定义和语义规则,并归纳关于[一等实体](#基本语义概念)和[一等对象](#基本语义概念)的性质。
除非另行指定,语言中不引入[非一等实体](#实体语义)。仅在特定局部上下文中操作非一等实体。
**原理**
限制非一等实体出现的规则有助于[统一性](#统一性)。
**注释**
根据[一等实体和一等对象](#一等实体和一等对象),[规则 G1a](#统一性) 是限制非一等实体的规则的推论。
一等实体的*一等(first-class)* 性质体现在语言支持的操作限制足够小,使之实例的全集可以涵盖任意[求值上下文](#上下文相关求值)中。
一个一等性质的反例是 \[ISO C] 的数组类型的[值](#基本语义概念)无法作为函数的形式参数。推论:\[ISO C] 的数组对象不是一等对象。
### 实体的等价性
*等价谓词(equivalence predicate)* 是判断[等价关系](../Terminology.zh-CN.md#计算机科学)的[谓词](#基本语义概念)。
等价谓词可定义一些[等价类](../Terminology.zh-CN.md#计算机科学)划分。
语言提供等价谓词判断两个项之间是否满足等价关系,满足判断等价关系的需要。
作用于[实体的值](#基本语义概念)的等价谓词(若存在)定义实体的*相等(equality)* 关系。
**注释** 这类似一般的值的集合上可能存在的相等关系。
决定相等关系的谓词是相等谓词,可判断实体和实体的值*相等(equal)* 。
除非另行指定,默认实体上的具体等价关系是[实体的同一性](#实体的同一性)。
对象语言不要求提供默认的具体等价关系,即任意两个实体不一定可以比较等价。
已知可比较等价的任意实体之间的等价关系也不具有唯一性。
一般地,设计等价谓词需注意避免一些现实的使用困难,如[关于相等性的困难](https://www.craigstuntz.com/posts/2020-03-09-equality-is-hard.html)。
为使等价关系在实体全集上[良定义](../Terminology.zh-CN.md#规范),等价谓词可能在特定情形弱化为同一性。
一般地,弱化应具有可用性理由,这可能和既有等价谓词和等价关系的蕴含的设计相关。
**原理**
等价谓词在避免依赖良序(well-ordering) 和良基(well-founded) 的理论中满足最小依赖原则,尽管其实现仍可能依赖序关系。
等价谓词的用途和上下文相关。
[同一性](#基本语义概念)在作为等价关系蕴含实体上的任何其它等价关系。被蕴含的等价关系可具有更多的限制条件。
实体的同一性是普遍的,但不是普适的,它仍不足以在所有上下文中都被关注。
同时,同一性无法保证在对象语言的所有实体上被实现;否则,会引起根本上显著的问题,限制[语言的可扩展性](#变化的自由),而和[通用目的语言的一般属性](#形而上学)冲突:
* 这类普遍同一性的具体判断依赖[外延性](https://zh.wikipedia.org/zh-cn/%E5%A4%96%E5%BB%B6%E6%80%A7),实质上是语言要求的相等性。
* 这要求任意具体的实现通过已知的、有限的方式构造,直接破坏[封装性](#封装)。
* 特别地,在[函数](#函数)等实体上的定义体现[函数外延性(function existionality)](https://ncatlab.org/nlab/show/function+extensionality) ,非常依赖具体实体的构造。
* 即便不考虑实现细节,如 λ 演算这样较为简单(但足够有[表达能力](#完整性)))的形式系统,一般的外延性即是[不可判定的](https://zh.wikipedia.org/zh-cn/%E6%B1%BA%E5%AE%9A%E6%80%A7%E5%95%8F%E9%A1%8C)。
* **注释** 这种不可判定使某些具体编码具有不唯一的表示,如 [Church 数(en-US)](https://en.wikipedia.org/wiki/Church_encoding#Use) 。
* 尽管有的系统如[公理化集合论](https://zh.wikipedia.org/zh-cn/%E5%85%AC%E7%90%86%E5%8C%96%E9%9B%86%E5%90%88%E8%AE%BA)通过[公理语义](#形式语义方法)(具体地,[外延公理](https://zh.wikipedia.org/zh-cn/%E5%A4%96%E5%BB%B6%E5%85%AC%E7%90%86))可以确切定义外延性,这同样依赖破坏封装的具体实现。
* 即便非通用目的语言也可能类似地因为外延性上的不可判定等问题,[存在充分理由拒绝默认提供函数外延性的等价谓词](https://github.com/coq/coq/wiki/extensional_equality)。
* 进一步,为满足[简单性](#简单性)和[可用性](#可用性),这自然地要求任意具体的实现是对象语言的[源代码](../Terminology.zh-CN.md#程序设计语言)中可表达,而破坏语言的可扩展性。
* **注释** 例如,语言中不能引入隐藏具体实现细节的[互操作](../Terminology.zh-CN.md#规范)。
* **注释** 作为反例,λ 演算中的对象都被 λ 项编码。但这引起的限制在通用目的语言中无法接受。
* 例如,[原生的算术](https://zh.wikipedia.org/zh-cn/%E9%82%B1%E5%A5%87%E6%95%B0)在[算法复杂度意义上即是低效](https://en.wikipedia.org/wiki/Church_encoding#Use)的,而通过机器数等方式的实现在复杂度和常数上都更高效。但后者的实现依赖外部系统,不能表达为 λ 项。
决定普适的等价谓词中蕴含的统一的等价关系是不可能的,因此语言中应允许共存多个等价谓词。具体等价谓词的设计可由派生实现及语言的用户提供。
等价谓词设计中弱化等价性的一个例子是 \[R6RS] 的[记录(record) 对象的相等性](http://www.r6rs.org/r6rs-editors/2005-August/000840.html)。
**注释**
[实体的同一性](#实体的同一性)是实体上的[可用于定义状态不变](#状态和行为)的等价关系的例子。它蕴含了实体没有被替换为不同的实体的判断,满足保持这种判断的[不变量](#阶段不变量约束)。
关于实体的关联的[值](#基本语义概念)是相对同一性更弱的等价关系。因为[不可分的同一性](#不可分的同一性),同一实体蕴含其值相等。
一些情况部分值的集合不满足数学意义上的等价(如浮点数的 [NaN](#数值类型) ),但在此忽略这种可被单独扩展的情况。
以下不同准则的操作是相等关系的实例(参见 \[EGAL] ):
* *抽象相等(abstract equality)*
* *[引用](#一等引用)相等(reference equality)*
* EGAL (\[EGAL])
#### 实体的同一性
[同一性](#基本语义概念)是实体上的等价关系的一个主要实例。
同一性决定的等价类之间的实体相同,即其整体和任意的[属性](#基本语义概念)在任意上下文中等价。
相同的实体在语言中不需要被区分,可直接替换而不影响程序的语义和行为。后者蕴含[可观察行为等价](#状态和行为)。
实体的同一性可体现在以下隐含的默认规则:
* 不同上下文的实体默认相互独立而不保证能被视为相同(在任意上下文中等价)。
* 通过语言构造引入的超过一个实体,默认为不相同的实体。
* 除非另行指定,表示具有同一性的实体的语言构造和其它实体不被要求[共享](#共享引用)指称相同的具有同一性的实体。
语言在[一等实体](#基本语义概念)上提供的同一性的[具体判断依据](#一等实体和一等对象)和具体语言支持的特性相关。
**原理**
同一性决定任意两个实体可在语言中不依赖具体操作的行为被直接区分,即满足 Leibneiz 法则(Leibneiz's law) ,或称为[不可分的同一性](#不可分的同一性)。
基于这个性质,可在实体上定义和 \[So90] 相容的更强的(不依赖语言设计中不存在副作用的)*引用透明性(referential transparency)* 。
同一性的引入默认是名义的,即断言具有同一性的实体和其它实体上的行为相互独立,而不需要附加证明。这种假设避免了一般地证明任意实体具有同一性的困难。
若不依赖直接在实体上标记等价类等依赖名义同一性假设的方法,证明一个实体具有同一性而非已知的其它实体,需证明任意的其它允许在程序中构造的实体和这个实体上的任意作用的可观察行为无关。在不限定具体的[计算作用](../Terminology.zh-CN.md#程序设计语言)属于会影响可观察行为的计算作用的确切集合时,这是计算上不可能的。因此,支持这类证明会有效地限制语言在支持不同的计算作用种类上的[可扩展性](#变化的自由)。
反之,从不同的对象上取消同一性(而允许实现共享资源等目的)一般是容易的:只要证明不存在影响可观察行为的计算作用即可。这种证明可以由程序名义地表达,例如标记某个实体上只涉及纯计算而没有副作用。
另一方面,这也提示纯计算在各种计算作用中具有的特殊性不足以使其作为唯一的可扩展配置的起点。
最平凡的起点应是没有任何计算作用的空计算。这无法表达计算,而必须要求扩展才具有实用性。而[通用目的语言](#需求概述)需要支持一般的计算作用,这同时包含支持纯计算。
从一般的计算作用排除副作用而得到纯计算,只需要添加可被系统证明的假设,这种机制可以嵌入到系统的规约规则中;而以支持纯计算的系统扩展表达一般的计算,需要引入不足以被对象语言求值规则描述其语义的间接表示(即需要被规约以外的规则翻译),并暴露更多和表达一般计算的目的无关的实现细节。
**注释**
按不可分的同一性,实体的属性在形式逻辑中通过量化的谓词判断而实现。
和不可分的同一性相对,存在同一性的不可分性(the indiscernibility of identicals) 。两者可被二阶语言形式地描述。
对象语言可提供同一性的相关操作,如:
* \[ISO C] 的非空对象指针的比较操作比较指向的相同类型对象的同一性。
* \[RnRS] 和 \[RnRK] 的 `eq?` 过程/应用子比较两个[操作数](#规范化中间表示)的同一性。
#### 实体的不可变性
[通过特定的等价关系可定义](#状态和行为)具体的[不可变状态](../Terminology.zh-CN.md#非自指)的集合。
这些集合可用于定义以这些状态为值的实体的*不可变性(immutability)* ,进而定义不保持可变性的[改变](../Terminology.zh-CN.md#非自指)操作和具体的其中可能影响[可观察行为](#状态和行为)的[修改](#基本语义概念)操作。
通过限定不同的修改操作,定义不同的*可修改性(modifiability)* 和对立的*不可修改性(nonmodifiability)* 。
通过明确不可修改性拒绝支持修改操作(例如通过通过实体的[类型检查](#类型检查)拒绝特定的修改操作),或通过不提供修改操作,语义规则保证实体不被修改操作改变状态。
**注释** 例如,关于 \[ISO C++] 的非类且非数组类型的[纯右值](#值类别)不可修改,尽管要求非纯右值的语义规则可被视为是一种类型检查。
(不依赖和影响[实体同一性](#实体的同一性)的)同一个实体上的修改操作是改变操作。只有具有[可变状态](../Terminology.zh-CN.md#非自指)的实体可能支持这些操作。
不论是否能区分同一性,实体可能关联不排除影响[可观察行为](#状态和行为)的可变状态。
一般地,一个实体不一定保证可区分是否具有不可变性以及具有何种不可变性(也蕴含一般不可区分可修改性),因为不可变性依赖实体的[表示](#表示)进行约定。
改变操作可能继续保持实体不变。
潜在引起实体的一些内部状态的变化的操作可不被视为影响不可变性而不被视为实体的(整体意义上的)改变操作。这种实体具有*内部可变性(interior mutability)* 。
可引起实体变化的状态按设计上是否[隐藏局部变化](#状态和行为)分为两类:
* *可变管理状态(mutable administrative state)*
* 可变管理状态的改变作为[管理状态](#状态和行为)的改变,不被视为对象(整体)改变的对象内部状态的改变。
* *可变数据状态(mutable data state)*
* 可变数据状态的改变是对象的改变。
[隐藏状态](#状态和行为)在可变性的意义上视为可变管理状态。
推论:
* 引起实体内的可变管理状态的改变的操作不一定是改变对象的操作。
* 引起实体内的隐藏的可变状态的改变的操作不一定是修改操作。
**原理**
基于等价关系而不是预设具体表示之间的相等定义可变性,避免抽象的目的(如[封装性](#封装))[依赖特定相等关系的实现细节](#避免抽象泄漏),支持[开放世界假定](#开放性)。
这种设计的一类典型反例是在预设排除副作用的[纯的](#求值性质)的设计为基础进行扩展定义改变操作,包括:
* 默认使用不可变[数据结构](#基本语义概念),并在此基础上扩展出可变的数据结构(如 \[Rust] )。
* 默认支持保证排除副作用的[纯求值](#求值性质),仅在有限的上下文中通过特定构造模拟支持非纯求值(如 Haskell 等纯函数式语言)。
一般地,这类策略对[通用目的语言](#需求概述)是过度设计,因为这实质上要求所有不存在改变操作的实体操作都完全排除副作用,不支持指定不同类别或层次保留不同改变操作并划分不同等价类的可能性,而限制表达的能力或增加实现相同抽象的复杂性。
关联可变状态的实体通常是对象,因为支持区分[同一性](#实体的同一性)而能支持发生在不同实体上的作用引起独立的状态的改变而分别影响可观察行为,但这并非绝对。只要允许构造出按等价关系判断具有不相同状态,非对象实体仍可支持内部可变性等不能排除影响可观察行为的性质。这不通过需要区分同一性的状态改变。
不区分同一性允许实现任选其中的实例代替其它实例。因此,在抽象机语义上依赖这些实体的不同等价状态表现的所有[良定义行为](../Terminology.zh-CN.md#程序设计语言)都应被允许,即未指定行为。
内部可变性同 \[Rust] 的 `RefCell` 等使用的模式以及 \[ISO C++] 的 `mutable` ,允许对象具有可变管理状态,而不影响依赖可变或可修改的对象整体意义上的类型检查。
和 \[Rust] 不同而和 \[ISO C++] 更加类似,这里的内部可变性仅限关于对象不可变性,和对象是否被[别名](#对象别名)正交(一些实例分析参见[这里](https://stackoverflow.com/questions/63487359))。
但是,和 \[Rust] 及 \[ISO C++] 都不同,这里不要求不可变性通过[类型检查](#类型检查)强制。
**注释**
可变和不可变的状态的区分类似 \[RnRK] 。
其它语言也遵循类似的设计。作为非对象实体的可变性的一个例子,[C++ 引用是否要求存储未指定](https://eel.is/c++draft/dcl.ref#4),尽管占用存储这一状态并非是语言支持的可变状态。这一规则直接允许 C++ 实现不需要依赖 [as-if 规则](https://eel.is/c++draft/intro.abstract#footnote-6)即可选取占用和不占用存储的方式实现引用的实例(乃至在运行时改变选取策略),即便是否占用存储可能对应 C++ 程序的不同的可观察行为。
改变或修改实体后,实体可能不变,即仍然具有和之前等价的状态。例如:
* 改变操作使用等价的状态替换先前的状态。
* 连续的改变操作使回复原始的状态,则这些改变操作的组合的作用不改变实体。
按[定义](#基本语义概念),蕴含引起表达式的值以外的改变的操作的作用是副作用。这里的改变是名义的,允许改变前后的状态等价。
支持不同等价的不可变性的一个用例是,有序的数据结构中的键需要保持的(通过序关系定义的)等价关系和键的可修改性是两种不同的等价关系。作为它的一个具体的反例,C++ 标准库要求关联容器的键具有 `const` 修饰,没有区分两种等价性,导致无法修改等价的键(除非具有 mutable 数据成员),而引起一些不必要的复杂。
### 实体的副本
在已知的实体以外,实体,作为其*副本(copy)* ,满足:
* 实体和实体的副本满足某种[等价](#实体的等价性),至少蕴含[实体的值](#基本语义概念)之间满足[不可变性](#实体的不可变性)。
* 若一个实体是对象,它的副本和它不[同一](#基本语义概念)。
除非另行指定,若实体的副本无法被创建,引起创建副本的操作[引起错误](#错误)。
若实体的副本可被创建,它可能通过:
* *复制(copy)* 实体:创建副本后,保持原实体的值[不变](#实体的不可变性)。
* *转移(move)* 实体:创建副本后,原实体被转移而具有*有效但未指定(valid but unspecified)* 的状态;若可能取得实体的值,其值[未指定](../Terminology.zh-CN.md#程序设计语言)。
* **原理** 要求有效,隐含其[访问](#基本语义概念)时具有[良定义行为](../Terminology.zh-CN.md#程序设计语言)。
* **注释** 参见[未指定状态](#状态和行为)。
* **注释** 这类似 \[ISO C++] [\[lib.types.movedfrom\]](https://eel.is/c++draft/lib.types.movedfrom) 。
* *析构性转移(destructively move)* 实体:创建副本后,原实体的[生存期](#基本语义概念)结束,不再可[访问](#基本语义概念)。
* 其它派生实现指定的创建实体副本的不同方式。
### 实体数据结构
实体的集合上可定义关联关系:集合的包含关系或其它实现定义的[严格偏序关系](../Terminology.zh-CN.md#计算机科学)。被关联的实体称为*子实体(subentity)* 。
子实体可以是作为[数据结构](#基本语义概念)的一部分。这种数据结构可以是一般的*图(graph)* 。
数据结构也可在对象语言中通过实体包含关系以外的途径定义。
**注释**
例如,限定包含关系构成的图中的[所有权关系](#所有权抽象)附加限制,详见[自引用数据结构和循环引用](#自引用数据结构和循环引用)。
### 续延
*续延(continuation)* 是特定[上下文](#上下文)中描述未来的[计算](../Terminology.zh-CN.md#计算机科学)的实体。
续延描述的计算可能在尚未发生的后继的规约中实现,在此之前可能被[调度](#基本语义概念),其中可指定不同的计算内容。上下文可决定这些计算中可变的参数化部分。
计算可通过切换续延蕴含[控制状态](#基本语义概念)改变而具有[控制作用](#基本语义概念)。
当前规约的上下文中对应的续延是*当前续延(current continuation)* 。
按对控制的限制,续延可分为*无界续延(undelimited continuation)* 和*有界续延(delimited continuation)* 等不同形式。
续延蕴含*子续延(child continuation)* 作为最后的一系列子计算。
形式上,若续延表示为一个计算步骤的序列,子续延的表示是续延的表示的后缀。
推论:无界续延的子续延是无界续延;有界续延的子续延是有界续延。
**注释**
续延可由符合[项规约系统](#项重写系统)的[规约步骤](#规约规则和求值)的集合或未指定的其它形式的[表示](#基本语义概念)。
不同形式的续延的调用都能具有类似的控制作用,但[表达能力](#完整性)不尽相同。
有界续延可从无界续延或有界续延通过添加续延的*界限(delimiter)* (或称为*提示(prompt)* )派生。派生的结果是原续延的一部分,表达原续延对应的计算的*子计算(subcomputation)* ,又称*部分续延(partial continuation)* 。
在仅使用*局部变换(local transformation)* 即 Felleisen *宏表达(macro-expressible)* (\[Fl91]) 的意义上,\[Fi94] 指出:
* 有界续延和可变状态(存储)可实现无界续延。
* 嵌入在语言中的任意表达[计算作用](../Terminology.zh-CN.md#程序设计语言)的*单子(monad)* 的构造可用有界续延实现。
### 一等实体和一等对象
NPL 区分两类不同的一等实体:只关心关联(作为对象时)的值的,和同时关心作为对象的[其它属性](#实体的同一性)的一等对象。
其中,后者允许更多的操作,且允许作为前者使用,反之无法直接保证:一等对象总是一等实体,一等实体不保证可作为一等对象使用。
逻辑上,一等实体可以关联其它对象(作为一等对象时关联可以是存储)。关联的对象的(表达式相关的)值是一等实体关联的值,可对应一等对象存储值。关联的值或存储的值是一等实体或一等对象的[属性](#基本语义概念)。
除非派生实现指定,NPL 的一等实体都是一等对象。
结合[实体语义](#实体语义)相关规则,存在推论:除非另行指定,语言中不引入非一等对象。
**原理**
一等对象的值是一等对象的属性。
一些设计中,显式地不区分对象和值,因为这些设计中不支持普遍的一等对象。在这些设计中,一等实体被称为一等对象。因为不保证提供其它属性,一等对象的值和一等对象也不再被区分。这有助于[简单性](#简单性),但阻碍[实体语义的可扩展性](#变化的自由),直接无法从语言设计中允许在一等实体中区分一等对象。因此,NPL 不使用这种设计。
**注释**
派生实现可以定义非一等对象的其它一等实体。
除非派生实现指定,非一等对象也不是一等实体。
显式地不区分对象和值如 \[RnRS] 。这些设计中,[值对象](#基本语义概念)若被使用,仍被作为实现细节;且因为互操作和允许支持副作用,值对象并非全部一等对象的内部表示。
#### 一等对象的同一性
一等对象通过保证具有[同一性](#基本语义概念)强调不相同的对象总是存在至少一种[总是不相同的属性](#实体的同一性)。
一般地,语言规则选取其中一种属性作为名义(nominal) 同一性属性。
一等对象具有名义同一性,定义为可比较名义同一性属性[相等](#实体的等价性);名义同一性的相等即名义同一性属性相等。
名义同一性在名义上标识相同的对象,区分不相同的对象,即便后者可能仍然在行为上完全符合同一性的要求。
形式上,一等对象是名义同一性属性和它作为一等实体的关联的对象作为非对象(无视同一性)的其它属性集合(如[存储的值](#表示))的二元组。
为简化设计,NPL 约定以下默认规则:
* 除非另行指定,名义同一性属性指定为对象在[抽象机语义](#实现行为)下的存储位置。
* 对象占据存储位置起始的若干存储。
* 存储位置的[表示](#表示)未指定;派生实现可指定具体的表示。
* 在语言规则中,一等对象满足[实体的同一性](#实体的同一性)的默认规则。
**原理**
由语言特性而非[用户程序](#程序实现)提供表达同一性的支持是必要的,这体现在通过在[通用目的语言](#需求概述)中省略同一性的表达再由实现或用户程序引入的做法一般是不可行的:
* 由 [Rice 定理](https://zh.wikipedia.org/zh-cn/%E8%8E%B1%E6%96%AF%E5%AE%9A%E7%90%86),非平凡(non-trivial) 的程序语义性质无法被[可计算地](#完整性)实现,而确定程序中任意对象的同一性蕴含判定“和特定程序行为一致”这种非平凡语义性质,无法被通过证明程序行为的等价或其中的[实体在任意上下文上的等价](#实体的同一性)任意地引入,因此若无法确定用户程序不需要任意的同一性(这是一种平凡情形),指定“不需要引入同一性”总是只能在特定的程序上由语言设计者或用户具体地决定。
* 作为通用目的语言若需要描述能适应语言自身实现问题的特性,总是依赖具体语言的逻辑上的[直谓(predicative) 的](https://zh.wikipedia.org/zh-cn/%E9%9D%9E%E7%9B%B4%E8%B0%93%E6%80%A7)规则(如资源抽象),除非语言规则是空集(这是一种平凡情形),这不可能完全由用户程序提供。
语言的设计中显式区分一等实体和一等对象的支持而非只直接支持一等对象仍然是必要的,主要原因是:
* 一等实体的具体表现形式通常是实现细节而要求不被依赖,为了支持前者不被显式表达,满足[关注点分离原则](#关注点分离原则)。
* 一等实体的普遍支持允许以[统一](#统一性)的方式抽象[可变状态](#状态和行为),且扩展使便于满足[变化的自由](#变化的自由)。
#### 可变状态和普遍性
NPL 对一等实体提供普遍的支持。
除非另行指定,NPL 不限制一等实体上可能具有的[作用](#基本语义概念),包括[副作用](#基本语义概念)。
**原理**
一等实体的普遍支持体现在:
* 在一般的一等实体上引入可变状态,实质上提供了*一等副作用(first-class side effect)* ,而不把[可修改性](#实体的不可变性)限于特定的数据结构(如[求值环境](#求值环境))。
* 允许以一致的方式和实现的外部环境进行[互操作](../Terminology.zh-CN.md#规范),特别地,允许物理上提供状态抽象的设备实体的状态直接映射为一等对象。
**注释**
特别地,一等对象默认支持可变状态。
派生实现可附加规则改变本节中对一等对象的默认要求,提供不同的保证或性质,包括[非一等对象](#一等实体和一等对象)上的其它一等实体上的不同作用。
### 同一性关联扩展性质
NPL 中,对象的同一性关联的属性包括明确开始和终止的[生存期](#基本语义概念)。
推论:对象是表示能明确生存期开始和终止的实体。
一等对象之间总是能准确地判断影响程序语义的[同一性](#实体的同一性):仅当能证明不改变[可观察行为](#状态和行为)时,两个一等对象的同一性可能未指定。
**原理**
通过一等对象关联同一性,允许语言提供依赖同一性差异的特性。
**注释**
同一性在这个意义上不是对象自身确定的性质(而是对象和解释对象表示的可能由外部提供的实现的共同确保的性质),不是应被隐藏的内部实现,因此 \[EGAL] 中有关自我诊断(autognosis) 的结论不适用;而代理(proxy) 仍然可通过语言提供适当的隐藏同一性的手段可靠地实现。
#### 一等状态
确保区分同一性的[状态](#状态和行为)是*一等状态(first-class state)* 。
一等对象能直接表示一等状态。
一等状态是否通过其它特性派生是未指定的。
**原理**
一等对象相对一等实体的附加规则限制集中体现在允许一等对象映射到的支持上。
注意并非所有一等对象都需要支持一等状态;否则几乎总是会[付出本不必要的代价](#避免不必要付出的代价)也难以避免违反[适用性](#适用性);因此有必要区分一等状态的对象和非一等状态的对象。
这种区分实质上更普遍地对具体的计算操作也存在意义,自然地引入了类似 \[ISO C++] 的[值类别](#值类别);最简单的设计如区分*左值(lvalue)* 和*右值(rvalue)* 分别关联是否需要支持一等状态的对象。
为支持一等状态,有必要支持判断两个对象的同一性,确保[修改](#基本语义概念)某个对象的操作不会关联到任意其它对象,以允许特定对象关联特定的一等状态。
为允许一等状态和[外部环境](#略称)的[互操作](../Terminology.zh-CN.md#规范),不能总是假定只有一类总是可被程序局部预知的[修改操作](#实体的不可变性)(典型地,定义为“设置[被引用对象](#一等引用)”操作,如 \[RnRK] §3.1 )影响状态,而应允许和特定对象关联的求值时的不透明的副作用。
若不考虑互操作,则一等对象用有限的不同[等价谓词](#实体的等价性)]即能提供区分同一性的操作;否则,等价谓词的设计即便保持[正交](#正交性),也需区分不同的一等对象对各种[副作用](#基本语义概念)的不同支持情况。
避免指定一等对象的可派生方式有助于[统一性](#统一性)。
基于 \[Fi94] ,结合可变状态能被表达为单子(如[这里](https://www.pauldownen.com/publications/delimited-control-effects.pdf))的事实,[有界续延](#续延)可实现状态。
相对地,基于 \[Fl91] ,[无界续延](#续延)和[异常](#异常)不能实现一般意义的可变状态,参见[这里](https://www.researchgate.net/profile/Hayo-Thielecke/publication/2487383_Contrasting_Exceptions_and_Continuations/links/53e12dba0cf2235f352738e5/Contrasting-Exceptions-and-Continuations.pdf)的推论 5.13 。
因为同一性可以在引入状态时被编码而在之后不需改变,使用有界续延等非一等的状态可支持实现状态的同一性。因此,在此不对是否基本要求作出限定。
但是,使用有界续延实现状态仅仅是实现细节,且通常具有一些非预期的实现性质:
* 这在控制状态和支持一等状态的实现之间建立的不对等(地位不同,相互之间交换后不等效)的偶然耦合;这种耦合不存在简化实现等益处而具有必要性。
* **注释** 例如,一等状态可能直接使用对应的*寄存器(register)* 实现。实现控制状态则通常需要更复杂的实现。
* 尽管理论可行,没有必要只是用其中一种作为另一种的实现的基础实现。
* 在现有实现普遍提供状态的原生支持(存储器)的常见情况下,单独通过其它方式编码状态反而会付出本不必要的代价。
* 这实质要求实现同一性无界续延具有区分同一性的能力(相当于 \[ISO C++] 的左值),而引起不正交的内部设计。
为满足非常规的实现环境或更优先的原则(如[变化的自由](#变化的自由)和[正确性](#正确性)),派生实现仍可使用有界续延派生一等状态,同时提供访问更基本的不依赖可变状态的接口,以使上述影响不再是非预期的。
[用户程序](#程序实现)仍不被禁止使用这种方式自行提供类似的实现,以确保[不约定一等状态作为基本的内建特性时](#统一性),语言的设计不违反 [G1b](#统一性) 。
**注释**
实现在一般实体上支持的[隐藏状态](#状态和行为)不被程序可编程地指定,不是一等状态。
允许和特定对象关联的求值时的不透明的副作用的一个实例是 \[ISO C] 和 \[ISO C++] 的 `volatile` 类型对象。
#### 一等作用
语言可指定特定的求值自动创建对象。
基于此规则可在传递时附加不同的[作用](#基本语义概念),即实现可随一等对象传递的*一等作用(first-class effect)* 。
**原理**
典型地,[按值传递](#求值策略)时,被传递后的对象和[实际参数](#函数合并)表示的对象具有不同的同一性,即按值传递时创建[新的对象](#实体的副本)。
基于被创建的[副本](#实体的副本)的[不变性](#实体的不可变性),这里的一等作用可包括用于维护对象的[不变性](../Terminology.zh-CN.md#自指)的[作用](#基本语义概念),[包括可能的副作用](#可变状态和普遍性),作为[契约式编程(programming by contract)](https://zh.wikipedia.org/zh-cn/%E5%A5%91%E7%BA%A6%E5%BC%8F%E8%AE%BE%E8%AE%A1) 的基础实现方式。
这种不变性可包括对象的生存期。通过限制特定表达式求值的[作用域](#基本语义概念)内销毁对象以确保对象生存期有限,即基于作用域的对象管理(scope-based object management) 。
基于作用域的对象管理可直接对应有限资源的普遍性质,使一等对象作为资源的抽象,确保资源的创建和销毁的副作用符合资源操作的语义,同时避免隐式的泄漏。
配合[一等状态](#一等状态),对象语言中的一等对象允许直接表示超过程序运行时自身的生存期的状态。这允许不在程序运行时持久储存的数据能直接被一等对象进行操作,而不需要依赖外部系统的约定并减少冗余操作(例如,从外部持久的“文件”上打开“流”以及其上的持久化操作),更符合[简单性](#简单性)。
**注释**
这里的资源抽象的惯用法在 C++ 中称为 RAII(resource aquisition is initialization) 。
#### 所有权抽象
配合[一等作用](#一等作用),实体的*所有权(ownership)* 自然地适用对抽象为对象的资源进行约束。
使用对象代表资源,则*所有者(owner)* 约束被其所有的其它对象的创建和销毁的时机。被所有的对象的生存期是所有者的生存期的并集的子集,且:
* 被所有的对象的生存期的起始不[先序](#规约顺序)所有者的生存期起始。
* 被所有的对象的生存期的终止不[后序](#规约顺序)所有者的生存期终止。
NPL 的设计避免要求对象语言隐含单一的*根(root)* 所有者作为其它资源的所有者。
**原理**
避免单一所有者适应抽象不同系统的需要,并满足[变化的自由](#变化的自由):
* 当不需要这样的所有者时,保持设计的[简单性](#简单性),同时满足[避免不必要付出的代价](#避免不必要付出的代价)和[最小接口原则](#最小接口原则)。
* 当需要这样的所有者时,仍然允许实现或派生实现引入。
注意[规约](#基本语义概念)允许蕴含[非一等对象](#一等实体和一等对象)的所有者用于提供规约时不在对象语言中可抽象为一等对象访问的资源,这样的所有者不需要是全局的;若实现为在不同规约实例乃至全局共享的资源,也不应在对象语言被依赖。
只要程序没有明确要求所有者,单一的全局所有者违反[最小依赖原则](#最小依赖原则),且不支持不清楚所有者状态时对特定对象之间进行所有权的局部推理(local reasoning) :
* 这种情形若不配合原始的明确目的(而间接明确资源的所有者)的设计说明,人类读者直接阅读实现理解和验证其正确性是困难的,即损失了可读性。
* 一种解决方式是读者自行模拟运行程序再从中推理出可简化的资源所有关系,这首先相当于要求读者模拟非确定性垃圾回收(GC, garbage collection) 的运行机制。这通常是困难的工作。
* 而机器通常更无法推理这些问题,因为设计和抽象的目的一般不是以机器可读的方式编码的。
* GC 可以回收资源,但无法准确统计哪些回收是必要的,也无法准确追溯原始实现并推理出应当在何种情况下静态地插入释放资源的操作,因为 GC 自始至终缺乏“允许任意延迟释放操作”以外的程序变换的[保持语义不变](#状态和行为)的证明所需的程序元信息(包括目的)。
为满足[变化的自由](#变化的自由),当需要表达局部所有权关系时,使用单一的全局所有者使用户无法直接在对象内嵌(embedding) 这种关系而需另行编码所有权信息,这存在以下问题:
* 使整体设计直接违反[避免不必要付出的代价](#避免不必要付出的代价)。
* 要求局部所有权以和全局默认机制的不一致的方式表达,损失[统一性](#统一性)并放弃[局域性](#关注点分离原则)而在满足需求时造成接口[抽象泄漏](#避免抽象泄漏)。
此外,即便使用时不要求区分对象的局部所有权关系,全局的分配释放机制也比局部的机制有更大的实现复杂性和约束。为实现对内部有限的资源的有效管理,局部所有权在实现中仍是必要的。
在使用全局所有者如全局的垃圾回收的实现中,这种必要性被隐藏在全局所有者内部实现,语言的整体设计不会更[简单](#简单性)。
使用全局所有者的资源管理假定启发式(heuristic) 策略以节约现实中无法接受的非预期性开销。这仍无法保证总是对不同的场景同样有效,以至于默认存在以下问题:
* 设计至少违反变化的自由和[简单性](#简单性)之一。
* 在不引入支持用户配置策略的扩充设计时,违反变化的自由总是无法避免的。
* 若引入其它设计支持用户配置策略,简单性违反难以避免,且实际基本上没有被避免。
* 即便能通过扩充设计避免违反简单性,也不能避免[不必要付出的代价](#避免不必要付出的代价)。
* 不论是否引入扩充设计,都会使资源管理的一般开销更难以估计,而使设计整体的可用性评估更困难,容易使用户决策和避免不必要付出的代价冲突。
### 一等引用
NPL 的一等对象即对象自身,不要求区分引用和*被引用对象(referent)* 的普遍概念。
反之,通过使引用和其它一些非引用的对象同为[一等对象](#一等实体和一等对象),NPL 支持作为一等对象的*一等引用(first-class reference)* 。
一等引用支持一等对象作为被引用对象。除非另行指定,若实现允许[非一等对象](#一等实体和一等对象)作为被引用对象,可作为被引用对象的非一等对象由实现定义。
特定的操作可能预期非引用,或总是隐含通过引用[访问](#基本语义概念)被引用对象,这不改变引用被作为一等对象使用的普遍支持。
一等引用的[相等关系](#实体的等价性)定义为被引用对象的[名义同一性相等](#一等对象的同一性)。
一等对象的使用仍然可以通过要求引用访问以避免在任意上下文中需要不同的对象副本。但这并不应排除其它形式的一等对象操作。
**原理**
尽管满足 \[RnRK] Appendix B 的准则(criteria) ,一等对象和 \[RnRK] 及 Java 等语言要求的设计不同。
注意有引用的语言的语义中不能排除被引用对象,否则无法确定引用对象的值的表达式的[求值结果](#基本语义概念)(例如来自对象[存储的值](#表示))以表达[计算](../Terminology.zh-CN.md#计算机科学);相反,无视引用而直接对值操作仍然能实现一些足够有意义的程序。
因此,若存在引用,无法忽略非引用(即便非引用不能在对象语言被直接使用)。
另一方面,引用可以由不指定为引用的一般对象上添加语义规则区分,而作为一般的对象的特例。
要求语言操作的一等对象总是关联到引用的设计实质上使对象语言的一等对象都是引用。但这不表示引用是自然的一等实体,因为引用的作用仅是操作被引用对象,不要求引用自身能被作为一等对象。
一等引用的相等性定义允许在相等的引用上推理[引用透明性](#实体的同一性)。
考虑此设计决策时关注的有以下几节中的依据。其它依据参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
#### 共享引用
共享引用是共享资源的引用。共享的资源(通常是存储空间)自身具有同一性,以*位置(location)* 标记。共享不同位置(即作为不同一等对象的)的引用可能引用同一个被引用对象。
**原理**
合理的共享引用可以节约实现占用的资源,提供更好的性能。但共享引用的实现仍可能有附加的开销,因此并不能保证使用共享引用一定能提供更好的性能。通常这种情形至少包括一些典型的对资源独占一次使用(具有独占所有权(unique ownership) )的情况。
更重要地,并非任意引用的共享都不改变程序的语义和行为,不合理的使用可能造成非预期的作用。
任意地引入共享引用而使用户不便预测其作用破坏[适用性](#适用性):
* 这包含直接破坏[易预测性](#易预测性),并在需要排除共享的场景中缺乏[可用性](#可用性)。
* 特别地,这和具有副作用的*非确定性(non-deterministic)* 编程冲突。
* 典型的[多线程并发执行](#并发实现)若需对象上的[副作用](#基本语义概念),需要保护和排除不必要的共享,确保独占[所有权](#所有权抽象)以避免*竞争条件(race condition)* 。
* 也有其它的一些类似的容易被忽略的[非确定性地破坏假设的场景](https://okmij.org/ftp/continuations/map-story.html)。
* 另见共享改变。
区分是否需要表达共享的情形一般不能由语言实现预知。和[使用全局所有者的问题](#所有权抽象)类似,使对象默认共享的设计若需避免违反[避免不必要付出的代价](#避免不必要付出的代价),在此相对不默认共享引用的设计违反[简单性](#简单性)。
默认共享引用可能是隐式的,即语言的实现不通过程序[代码](../Terminology.zh-CN.md#程序设计语言)中的显式标注的操作而引入共享的引用,且往往无法保证通过一等对象上的操作避免被引用的对象被其它一等对象引用——无法使用对象语言的操作排除共享引用(即便是新创建的对象也没有保证,尽管实现上不必要)。
在要求一等对象都是引用的设计中,一般地,只有不要求名义同一性的非对象的实体才能安全地共享引用,但在非对象实体上的类似引用的机制并没有保证通过一等引用提供为语言特性。
其它情形中,允许引用之间的隐式的共享使[不相同的对象](#一等对象的同一性)可能共享状态而破坏[同一性的行为保证](#一等对象的同一性):程序无法可靠地避免共享状态导致的对[可观察行为](#状态和行为)的影响,此时共享状态的改变非预期地影响其它对象,其行为不具有[一致性](#一致性)。
为了排除破坏同一性和适用性的问题,语言的设计需要限制引起问题的操作的可用性(例如,\[RnRK] 和 \[RnRS] 不提供使用一等引用的改变操作以保证变化能通过程序[源代码](../Terminology.zh-CN.md#程序设计语言)中有限的语法上下文被推理),但这样的策略限制设计的[通用性](#其它推论和比较)。
因为共享引用的影响的普遍性,不提供可避免隐式共享引用的设计的造成的缺陷也是普遍的。
由于显式的引用可以由用户控制在局部使用,更容易推理其影响,可避免类似的缺陷。
关于共享改变和程序无法可靠地避免共享状态导致的对可观察行为的影响,参见参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**注释**
一些语言的设计指定或隐含的规则在程序代码操作的一等对象上普遍地引入隐式共享的引用,如:
\[RnRK] 中的引用和被引用对象明确地分离,且 `$define!` 和 `set-car!` 等[改变操作](#实体的不可变性)要求设置对象引用的其它对象为特定的[操作数](#规范化中间表示)确定的被引用对象,无法排除被设置的引用被共享;这实质要求所有可能包含其它引用的可被改变的对象中的引用都需要能构成隐式的共享。
\[RnRS] 明确指出特定的空对象的唯一性(即便因为不保证具有位置,不一定保证以位置决定的[名义同一性](#一等对象的同一性)),蕴含这些对象上总是可构造或超过一个引用必须构造隐式的共享引用;其它变量引用(variable reference) 未指定排除隐式的共享。
##### 对象别名
除非在语言规则中添加复杂的约束(如通过[类型](#类型)的机制)以证明特定上下文可避免共享引用,无法避免引用引入不必要的对象*别名(aliasing)* 。
若公开这样的性质作为接口约束,违反[最小接口原则](#最小接口原则)。
隐式的共享使涉及[修改](#基本语义概念)的操作的特性更难设计,参见共享改变。
关于共享改变,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**原理**
对象别名一旦引入,通常难以在所有被别名的对象生存期结束前消除。
证明对象不被别名是困难的,因为这逻辑上要求在局部知悉所有被别名的对象的存在性,而不具有[局域性](#关注点分离原则)。
### 自引用数据结构和循环引用
特定的数据结构在逻辑上要求内部具有相互指涉的引用,即*自引用(self-referencing)* 。
自引用可实现为一等对象集合内的*循环引用(cyclic reference)* ,即允许对象属于有限次迭代访问被引用对象的操作的传递闭包(非空的*链(chain)* ,称为引用对象链)的构造。
NPL 的不保证支持这种方式实现自引用。
**原理**
NPL 的设计不保证支持通过循环引用实现自引用,以避免一些固有缺陷。即便派生语言允许提供扩展支持,但本节讨论的原理仍然适用。
避免自引用的构造使实体构成的数据结构由一般的[图](#实体数据结构)退化为(可共享节点的)树形数据结构,即 DAG(Directed Acyclic Graph ,有向无环图)。
这样的设计在实现上避免外部所有者(如[全局 GC](#所有权抽象) )。
避免一般的循环引用的普遍理由是:[*非直谓性(impredicativity)*](https://zh.wikipedia.org/zh-cn/%E9%9D%9E%E7%9B%B4%E8%B0%93%E6%80%A7) 并非是抽象上必要的普遍特性。一般的循环引用在抽象上即应通过特殊进行归纳,这并非[泄漏抽象](#避免抽象泄漏)。
反之,需求决定的抽象上不必要的情形下,假定循环引用的存在反而妨碍抽象的构造,可能避免某些有用的普遍性质(例如,保证程序可终止;另见[强规范化性质](#范式)),而违反[简单性](#简单性)、[统一性](#统一性)和[适用性](#适用性),并引起若干具体设计问题。
关于通过任意对象支持循环引用的问题,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
#### 一般引用数据结构的一般实现
通过一些替代原语,在不支持循环引用的情形仍可支持自引用数据结构。
语言可以提供在不支持一般的循环引用的对象构造中保存无所有权的一等实体引用其它实体,构造出不蕴含所有权的仅以特定对象构成的循环引用,而在外部引入对象作为所有这些构成引用的对象的所有者的机制。
在这个基础上,一般的自引用或循环引用需要的附加指涉仍然可通过添加不蕴含所有权语义的引用解决。这些引用是*弱引用(weak reference)* ,区分于具有所有权的引用是*强引用(strong reference)* 。
强引用总是可转换为弱引用使用。弱引用通过*解析(resolve)* 取得强引用。解析可能失败,以允许弱引用指涉已经不存在的对象,而避免影响对象[生存期](#基本语义概念)和所有权关系。
若支持这种受限形式的循环引用,具体特性由派生实现定义。
**原理**
没有理由表明通过任意对象支持循环引用是自引用数据结构的唯一实现方式,不论使用自引用数据结构的普遍程度。
自引用数据结构可通过在更高的抽象层次上编码,转换为由用户(而不是语言实现)指定明确的外部所有者的形式消除上述所有问题,同时对外部保证同等的功能[正确性](#正确性)。
使用受限的循环引用同时避免带有所有权的循环引用也是 C 和 C++ 等语言惯用的实现图(graph) 的数据结构的合理方式。
### 实体类型
NPL 不要求预设具体的实体及对象类型的设计,因此不要求用户使用语言体现整体上的[可扩展性](#统一性)。
特别地,NPL 不要求表达式具有预设的不同[类型](#类型)。
**原理**
放弃对预设类型的要求允许由派生实现指定类型的外延而满足[变化的自由](#变化的自由)。
除不必涉及[引用](#一等引用)外,\[RnRK] 中定义的封装的(encapsulated) 类型的概念及类型[封装性](#封装)( \[RnRK] 原则 G4 )仍然适用,且一般仍然需要满足;差异是派生实现因为扩展不满足的情形也不影响此实现的[一致性](#规范模型)(尽管使用扩展的程序可能不可移植)。
尽管[值类别](#一等状态)可抽象为特殊的类型,表达式中的对象的类型和值类别的规则应分别讨论,因为两者正交:两者的[确定](#类型)和[检查](#类型检查)机制都相互独立。
## 名称规则
[名称](#名称)和能标识特定含义、符合[名称词法约束](#词法规则)的表达式一一对应。
具体的外延由派生实现定义。
表示名称的表达式不同于名称,但在无歧义时,语言中可直接以名称代指表达式和对应的词法元素。
[求值算法](#求值规约)中对名称的处理应满足本节的要求。
**原理**
名称规则约定通过程序[源代码](../Terminology.zh-CN.md#程序设计语言)确定的静态的语法性质。
部分规则中的概念定义和约定仅为便于描述这些性质。和这些约定对应的结构不一定需要在求值算法的实现中出现。
### 声明区域约定
对引入名称 `n` 的[声明](#基本语义概念) `D` ,对应的[声明区域](#基本语义概念)始于紧接 `n` 的位置,终于满足以下条件的[记号](#基本词法构造)`)`(若存在)或[翻译单元](#基本文法概念)末尾(不存在满足条件的记号 `)` ):
* 记号 `)` 和与之匹配的记号 `(` 构成的表达式包含 `D` 。
* 此记号之前不存在满足上一个条件的其它的记号 `)` 。
### 可见名称
名称隐藏规则:若[声明](#基本语义概念) `D` 是表达式 `E` 的子集,且不存在 `D` 的子集声明同一个名称,则 `D` 声明了[有效名称](#基本语义概念),隐藏了 `E` 中其它同名的名称。
在[声明区域](#基本语义概念)中,没有被隐藏的名称是*可见(visible)* 的。有效名称实质蕴含可见名称。
### 名称解析
*名称解析(name resoultion)* 是通过名称确定名称指定的[实体](../Terminology.zh-CN.md#程序设计语言)的操作。
不保证名称解析总是成功。
除非另行指定,成功的名称解析没有[副作用](#基本语义概念)。
除非另行指定,直接作为求值算法步骤的不成功的名称解析[引起错误](#错误)。
一般地,名称解析包括*名称验证(name verification)* 和*名称查找(name lookup)* 两个阶段。
名称验证确定名称是[可见名称](#可见名称),同时可能排除部分无效名称。
名称查找进一步确定名称唯一指称的实体的(蕴含确定名称有效),仅在名称验证成功后进行。
不同名称经过名称查找的结果可能等效。等效的有效名称视为[同一的](#基本语义概念),规则由派生实现定义。
名称解析从保存名称的目标中查找名称。若查找失败,解析可继续从替代的其它目标中进行。这种机制称为*重定向(redirection)* 。重定向后的解析可继续包含名称验证和名称查找的步骤。
以上约定以外的具体规则以及失败的行为由派生实现定义。
### 命名空间
*命名空间(namespace)* 是[实体](../Terminology.zh-CN.md#程序设计语言)。命名空间可以由名称指称。
是否实现命名空间为程序中可由用户指定可变的实体及[求值环境](#求值环境),由派生实现定义。
#### 命名空间指称
总是没有名称*指称(denotation)* 的命名空间是*匿名命名空间(anonymous namespace)* 。
没有有效名称指称的命名空间是*未命名命名空间(unnamed namespace)* 。
NPL 定义一个抽象的匿名命名空间,称为*根命名空间(root namespace)* 。未命名命名空间的支持由派生实现定义。
NPL 约定一个在实现中的有效名称总是指称一个命名空间。有效名称指称的命名空间的[同一性](#基本语义概念)和[有效名称的同一性](#名称解析)对应。
**注释**
匿名命名空间和未命名命名空间不同。前者可能是一个系统的默认约定,一般整体唯一存在(如*全局(global)* 命名空间);后者只是对某些接口隐藏,可以有多个。
#### 命名空间成员
除了用于指称的名称外,一个命名空间可以和若干其它名称关联。
通过派生实现定义的对命名空间的操作可以取得的名称是这个命名空间的*成员(member)* 。
若无歧义,命名空间的成员指称的实体也称为这个命名空间的成员。
命名空间直接包含成员,称为直接成员。
除了根命名空间和其它派生实现定义外,命名空间可以作为另一个命名空间的成员,此时命名空间内的成员(若存在)是包含其的命名空间的间接成员。
命名空间对成员的直接包含和间接包含总称为包含,是反自反的、反对称的、传递的二元关系。
#### 简单名称和限定名称
命名空间的直接[成员](#命名空间成员)的标识符在这个命名空间中是有效名称,称为*简单名称(simple name)* 。
命名空间及其成员按包含关系依次枚举标识符组成的序列是一个名称,称为在这个命名空间中的*限定名称(qualified name)* 。
[根命名空间](#命名空间指称)的限定名称称为*全限定名称(fully qualified name)* 。
限定名称的语法由派生实现定义。
**注释**
限定名称的语法的一个实例是标识符之间作为逻辑上的[分隔符](#分隔符)的[记号](#基本词法构造)。
## 规约规则和求值
对象语言的[操作语义](#形式语义方法)可通过作为计算模型的[项规约系统的规约规则](#项重写系统)中由规约规则描述的规约*步骤(step)* 指定。
除非派生实现另行指定,规约蕴含 NPL 程序的执行,可完全表示程序执行的语义。
推论:NPL 规约规则形式地蕴含 NPL 语义规则。
为表达明确的目的,[语言规则](../Terminology.zh-CN.md#规范)也可约定[其它更抽象形式的求值规则](#可选求值规则),以蕴含这些规约规则,而不是直接描述规约规则的形式语义。
描述 NPL 对象语言的操作语义也可被视为[特定的对象语言](#实现行为),其规约可以视为求值。但除非另行指定,以下表达式仅指对象语言的表达式,其求值仅指关于对象语言中表达式的求值,而非一般的规约。
规约规则可要求被规约的项符合一定的结构(如具有特定类型的值)作为前提,否则规约出错,程序执行中止。
根据规约规则描述的行为是否对应对象语言中的求值,规约分为两类:表达式的[求值规约](#求值规约)和[管理规约](#管理规约)。
### 求值规约
一个规约可以描述表达式的求值。直接表达一个表达式求值的规约是一个求值规约。
以[项重写系统](#基本语义概念)描述,求值规约的输入是作为表达式的[表示](#表示)的[项](#基本语义概念),称为*待求值项(evaluating term)* 。
待求值项经求值规约取得[求值结果](#基本语义概念)。
除非另行指定,求值结果是通过[值计算](#基本语义概念)取得的值。
**原理** 求值结果也可能是[异常](#异常)退出的等其它作用对应的实体。这些求值结果可能需要派生实现定义的不同规则的处理。
以下项称为*被规约项(reduced term)* :
* 待求值项。
* **注释** 因为可附加等价[空求值](#求值性质)的恒等规约,不需要区分项是否已被规约。即使表达式从未被求值,其表示也可视为待求值项。
* 规约步骤的[中间表示](#实现的执行阶段)中完全依赖求值规约的输入的子集的项。
* 表示[求值结果](#基本语义概念)的项。
求值规约规则构成对象语言的*求值算法(evaluaton algorithm)* 。
求值算法的输入是被求值的表达式和支持[上下文相关求值](#上下文相关求值)中的上下文。
求值的基本操作以满足特定规则的*替换(substituion)* 规则或其组合表示。
除非另行指定,以下讨论的排除求值副作用的重写系统具有[汇聚性](#演绎规则)。
这保证求值满足值替换规则:表达式的[值计算](#基本语义概念)通过已知的[子表达式](#表达式)的[值](#基本语义概念)替换决定。
除非派生实现另行指定,子表达式的值仅由求值得到。
**注释** 此时[递归蕴含规则](#递归蕴含规则)中的求值依赖规则是这个规则的推论。
### 管理规约
求值规约以外的规约称为*管理(administrative)* 规约。
管理规约可以是一个不完整的求值规约,或者和求值规约的步骤没有交集。
管理规约可使用对象语言不可见和不可直接操作的非[一等状态](#一等状态)的[管理状态](#状态和行为)。
表示非[一等对象](#基本语义概念)的项的规约总是管理规约。
[抽象求值](#基本语义概念)中不在对象语言[求值结果](#基本语义概念)中可表达的中间规约是管理规约实现。
**注释**
管理规约描述语言的表达式以外的[操作语义](#形式语义方法)。
实现也可使用的管理规约描述特定于实现的(而在对象语言中未指定的)语义性质。
### 规约顺序
*先序(sequenced before)* 关系是两个规约之间存在的一种[严格偏序关系](../Terminology.zh-CN.md#计算机科学),对实现中规约之间的*顺序(order)* 提供约束。
*后序(sequenced after)* 是先序的逆关系。
*非决定性有序(indeterminately sequenced)* 是先序或后序的并集。
*无序(unsequenced)* 是非决定性有序在求值二元关系全集上的补集。
规约规则的顺序直接适用于求值,其顺序为*求值顺序(evaluation order)* 。
规约规则的顺序也适用在能以其形式描述相对顺序的*事件(event)* 上。[程序](../Terminology.zh-CN.md#程序设计语言)中蕴含的这些事件称为*规约事件(reduction event)* ,包括:
* 求值。
* 对象的[生存期](#基本语义概念)的起始或终止。
* [副作用](#基本语义概念)的[起始](#作用使用原则)和[完成](#作用使用原则)。
* 不涉及副作用的完成[在求值结束后存在](#作用使用原则)时,某个[计算作用](../Terminology.zh-CN.md#程序设计语言)的存在。
* 派生实现定义的其它事件。
一些事件的顺序是通过推理具有*因果性(causality)* 的*依赖(dependency)* 关系决定的,包括:
* 规约中[值计算](#基本语义概念)依赖规约的输入,即被求值的表达式和其它可能影响规约的状态。
* 被副作用的起始决定的其它作用依赖这个副作用。
* 从一个实体上确定作为值的属性的*读(read)* 依赖这个属性。
* 在一个实体上可以作为值保留的属性的*写(write)* 被这个属性依赖。
* 由派生实现定义的其它情形。
**注释** 外部[表示](#表示)作为实体的读取和写入是这里的属性的特例。
为了确定相关的值,依赖关系可直接替换为后序关系。
由二元关系的一般性质(特别地,偏序关系的传递性),可推导其它一些事件之间的确定顺序,如同一个实体属性上的读依赖(已知的)决定了这个属性的先前的写。
作为先序和后序的扩展,规约事件可符合*在先发生(happens before)* 和*在后发生(happens after)* 的严格偏序关系,满足:
* 对同一个[执行线程](#并发实现)中的事件,在先发生和在后发生分别同先序和后序。
* 组合在先发生或在后发生的关系的不存在*环(cycle)* 。
* 派生实现定义的其它要求。
NPL 约定以下非决定性规约规则:除因果性和二元关系的一般性质的推论外,任意项之间的规约之间无序。
应用在求值顺序上,有以下推论(非决定性求值规则):除因果性和二元关系的一般性质的推论外,任意表达式的求值之间无序。
**原理**
在先发生和在后发生可描述系统中的[并发的](../Terminology.zh-CN.md#计算机科学)事件。[原始定义](https://www.microsoft.com/en-us/research/uploads/prod/2016/12/Time-Clocks-and-the-Ordering-of-Events-in-a-Distributed-System.pdf)包括对*时钟(clock)* 的抽象,但此处不要求指定。
\[ISO C++] 和 \[Rust] 等使用类似的方式描述并发的求值的支持。这些设计中,不同执行线程中具有特定的操作定义具体的顺序关系。其中具体规则的设计可能不同而不保证完全一一对应。
因[可扩展](#变化的自由)和[简单性](#简单性) NPL 不在此明确指定此类具体操作,而由派生实现定义。
及非决定性规约规则允许在语言中表达[并发实现](#并发实现)。
**注释**
读和写作为影响可观察行为的事件结果,具有因果性。此外,也可以抽象为计算作用并由程序操作;这里不做要求。
### 求值性质
两个具体求值等价,当且仅当两者的作用相等。
两个求值等价,当且仅当作为具体求值时等价,或其中每个求值的变换实质蕴含另一个。
没有[副作用](#基本语义概念)的求值是*纯的(pure)* 。
**注释** 推论:纯求值仅有[值计算](#基本语义概念)或抽象求值。
值为被求值的表达式自身的具体求值或不包含变换为存在不等价求值的表达式的抽象求值为*恒等(identity)* 求值。
恒等的纯求值是*空求值(empty evaluation)* 。
作用是空集的表达式求值是*空作用求值(null effect evaluation)* 。
**注释** 推论:空作用求值是空求值。
[语法形式](#语法形式)固定且求值总是空求值的表达式是*空表达式(empty expression)* ,这仅由派生实现可选提供。
### 范式
*规范化形式(normalized form)* ,或简称*范式(normal form)* ,是由派生实现定义的[表示](#表示),被一组[规约](#基本语义概念)规则确定,满足:
* 通过有限的规约步骤后得到。
* 按规约规则,规范形式上不存在不[和空求值等价](#求值性质)的进一步规约。
在具有 [Church–Rosser 属性的重写系统](#演绎规则)中,一个对象若具有范式则唯一。
表达式在得到规范形式后规约终止,且蕴含求值终止。
得到范式的规约步骤称为*规范化(normalization)* 。
若表达式规约总是能得到规范形式(求值总是能在有限规约步骤后终止),则具有*强规范化(strong normalization)* 性质。
实现应避免引起对象语言的语义表达以外的无法保证强规范化性质的操作(如直接无条件的递归规约调用)。
除非派生实现另行指定,不保证强规范化性质。
保证得到范式的规约是规范化规约。
[具体求值](#基本语义概念)得到的范式若可作为表达式,其[求值结果](#基本语义概念)是和被求值的项等价的表达式的[值](#基本语义概念),即仅允许[恒等求值](#求值性质)而仍是范式;这样的项称为*自求值项(self-evaluating term)* 。
作为表达式的自求值项是*自求值表达式(self-evaluating expression)* 。
重复求值直至取得自求值项的求值结果是*最终求值结果(final evaluation result)* 。
**注释**
推论:最终求值结果上可能的求值是[纯求值](#求值性质)。因此,取得最终求值结果后,即排除具有[副作用](#基本语义概念)的继续求值。
### 规范化中间表示
第一个[子表达式](#表达式)(头表达式)是[范式](#范式)的表达式是 HNF(Head Normal Form ,头范式)。
头表达式是可直接求值为范式的表达式是 WHNF(Weak HNF,弱头范式)。
**注释** 约定求值到 WHNF 提供保证[强规范化性质](#范式)的一般手段,可用于[非严格求值](#严格性)。
WHNF 的头表达式是*操作符(operator)* ,存在对应 HNF 的头表达式的[最终求值结果](#范式)。
**注释** 详见[合并子](#合并子)。
WHNF 中除了操作符以外的子表达式是*操作数(operand)* 。
操作数以具有限定顺序或不限定顺序的数据结构表示。
按操作数的数据结构对应有*操作数列表(operand list)* 和*操作数树(operand tree)* 。其中操作数树是有限的树形数据结构的 [DAG](#自引用数据结构和循环引用) ,其具体构造和表示由派生实现定义。
**注释** 操作数树和 \[RnRK] 类似。语言可能进一步约定有序的数据结构表示操作数的组成部分之间在求值上不等价。
这种能以操作符和操作数的组合表达的计算形式是*操作(operation)* 。
操作的*结果(result)* 是表达规约步骤得到的范式;操作的[作用](#基本语义概念)是取得对应结果的规约步骤的作用。
**注释** [函数合并](#函数合并)的[求值结果](#基本语义概念)中可蕴含操作的结果,也可[具有其它作用](#程序的控制执行条件)。若操作的结果存在,则同时是这个合并子的[调用](#函数调用)的结果,即[返回值](#函数调用)。
若操作的结果不依赖[管理规约](#管理规约),操作的结果和作用即这种可求值为 WHNF 表达式的[求值结果](#基本语义概念)和作用。
**注释** 另见[函数值](#函数合并)。
关于 DAG ,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
### 组合求值
表达式和[子表达式](#表达式)之间的求值需满足一定约束。
#### 递归蕴含规则
表达式和子表达式之间的求值满足以下递归蕴含规则:
* 求值依赖规则:除非另行指定,表达式被求值实质蕴含子表达式被求值。
* 顺序依赖规则:求值子表达式的[值计算](#基本语义概念)[先序](#规约顺序)所在的表达式的值计算。
* 平凡求值规则:指定一个表达式是[纯求值](#求值性质)或[空求值](#求值性质)对应实质蕴含其子表达式的求值被指定为纯求值或空求值。
**注释**
一般地,一些[求值策略](#求值策略)可以不遵循求值依赖规则。
顺序依赖规则是[因果性](#规约顺序)的具体表现之一。对不被求值的表达式,此规则不生效。构造不同的表达式进行计算可实现和直接违反此规则等效的作用,但因为是不同的表达式,实际上不违反此规则。
附加的顺序依赖规则可由特定的实体构成的表达式的求值隐含指定。相同的表达式可能在不同上下文中使用不同的规则。
#### 严格性
若表达式的任意子表达式的求值总是[非空求值](#求值性质)且[先序](#规约顺序)表达式求值,则这个表达式的求值是*严格的(strict)* ;反之,求值是*非严格的(non-strict)* 。
推论:严格求值满足[顺序依赖规则](#递归蕴含规则)。
非严格求值在规约时可保留未引起作用(通常即未被求值)的部分子表达式,允许实现根据先序的求值作用确定的选择性求值,即包括未指定是否作为[空求值](#求值性质)的子表达式求值,如分支判断或短路求值。
**注释** 例如:ISO C++ 的条件表达式存在可能未被求值的[操作数](#规范化中间表示),属于非严格求值;`++` 表达式不作为*完全表达式(full expression)* 时,副作用可超出此表达式的求值(不满足顺序依赖规则),也是非严格求值。
表达式经过*严格性分析(strictness analysis)* 确定是否严格求值,通过*严格性分析器(strictness analyzer)* 在[语义分析](#实现的执行阶段)时实现。
*中间值(thunk)* 是保留不直接实现[具体求值](#基本语义概念)的部分子表达式的特定的数据结构。
**注释** 例如,通过保留中间值待延迟求值,可实现子表达式值的[按需传递](#求值策略)。
#### 顺序求值
明确的词法顺序可为同一个表达式的若干子表达式提供一致的有序[求值策略](#求值策略):从左到右或从右到左。为一致性,不需要考虑其它特定顺序作为一般规则。
递归文法表示的表达式和子表达式之间存在相对内外顺序:子表达式在表达式的内部。此求值顺序可对应表达式树的遍历顺序。
#### 替换策略
对应项的规约规则的表达式的重写规则由派生实现定义,典型的可选项包括:
* 名称替换:保证替换前后项对应的[名称](#名称)不变。
* 实体替换:保证替换前后项关联的实体不变。
* 值替换:保证替换前后项关联的表达式的值满足实现定义的相等关系。这包括以下不同的变体:
* 值副本替换:保证替换前后项关联的表达式的值满足值替换的关系,且以实现定义的方式引用不同的[实体的副本](#实体的副本)。
* [引用](#一等引用)替换:保证替换前后项关联的表达式的值满足值替换的关系,且以实现定义的方式引用同一实体。
#### 求值策略
组合严格、顺序求值和替换策略可得到不同性质的求值策略。
除非派生实现约定,表达式求值策略可以随具体[语法形式](#语法形式)不同而不同。
典型性质组合如下:
* 严格求值:
* 应用序(applicative order) :以*最左最内(leftmost innermost)* 优先的顺序求值。
* 最左的顺序仅在操作数是[有序数据结构](#规范化中间表示)时有意义;不考虑操作数内部构造时,仅表示操作数作为子表达式总是被求值,和严格求值等价。
* 按值传递(pass by value) :使用值替换的严格求值。
* 按值的副本传递(pass by value copy) :创建[值的副本](#实体的副本)进行替换的严格求值。
* 按[引用](#一等引用)传递(pass by reference) :使用引用替换的严格求值。
* 共享对象传递(pass by shared object) :使用的共享机制以及对象和值或引用的关系由派生实现定义。
* 部分求值(partial evaluation) :允许求值分为多个*阶段(phase)* 分别进行。
* 非严格求值:
* 正规序(normal order) :以*最左最外(leftmost outmost)* 优先的顺序求值。
* 最左的顺序的意义同应用序。
* 按名传递(pass by name) :使用名称替换且保持作为名称的表达式最后被替换的求值。
* 按需传递(pass by need) :按名传递但允许合并作用相同的表达式。
* [非决定性](#规约顺序)求值:
* 完全归约(full reduction) :替换不受到作用之间的依赖的限制。
* 按预期传递(pass by future) :并发的按名传递,在需要使用参数的值时同步。
* 乐观求值(optimistic evaluation) :部分子表达式在未指定时机部分求值的按需求值,若超出约定时限则放弃并回退到按需求值。
### 可选求值规则
应满足的本节上述约定的最小求值规则和语义外的具体求值的规则和语义由派生实现定义。
派生实现的求值可满足以下节指定语义,此时应满足其中约定的规则。
若可选求值规则逻辑上蕴含规约规则,则被蕴含的规约规则的直接表述可在语言规则中被省略。
### 上下文相关求值
在被求值的表达式以外,对应的规约规则在实现此规约的[元语言](../Terminology.zh-CN.md#程序设计语言)中可能是[上下文](../Terminology.zh-CN.md#自指)相关的,这种附加依赖的上下文为*求值上下文(evaluation context)* 。
求值上下文被作为元语言实现对象求值规则时的输入,可指定项所在的位置等不被被规约的项必然蕴含的附加信息。
由派生实现定义的特定求值上下文称为*尾上下文(tail context)* 。以尾上下文求值可提供附加的保证。
作为[项重写系统的上下文](#项重写系统)的实例,元语言中,一般的求值上下文 `C` 形式化为具有占位符 `[]` 和可选前缀 `v` 及可选后缀 `e` 的递归组合的串:
```xbnf
C ::= [] | Ce | vC
```
其中 `e` 是被求值表达式,`v` 是作为[范式](#范式)的值。
除非另行指定,NPL 对象语言的[求值算法](#求值规约)使用的求值上下文总是[求值环境](#求值环境)。
**原理**
通过附加适当的求值规则保证对象语言中的表达式总是可唯一地被分解为这种表示,抽象的求值上下文可直接实现对象语言的求值。但语义描述和实现的基准都以[抽象机](#实现行为)替代,因为:
* 抽象机语义允许不依赖[源程序](../Terminology.zh-CN.md#程序设计语言)的[表示](#表示)和构造(如特定的表达式的文法)。
* 这种分解一般要求遍历对象语言的源程序而难以具有较好的可实现性质,如[计算复杂度](../Terminology.zh-CN.md#计算机科学)。
* 为满足良好的可实现性质,需描述实现中可能具有的离散状态与只和其中个别状态关联的局部的求值规则时,这种分解通常会渐进演化为某种抽象机的表示。
**注释**
使用求值环境作为默认的上下文确保一般的求值总是能支持变量的[绑定](#基本语义概念)。
对象语言的实现同时能够支持其它上下文,即使它不在求值算法中出现。这样的上下文可能被求值上下文蕴含而可被推理确定。
## λ 完备语义和对应语法
作为通用语言,求值规则表达的系统可具有和[无类型 λ 演算](#模型)对应的形式和计算能力。
基于此语义的派生实现应允许以下几种互不相交的表达式集合:
* [名称表达式](#名称表达式)
* [匿名函数](#函数)
* [函数应用](#函数合并)
NPL 不要求以上表达式中函数以外的表达式求值的[强规范化](#范式)。
**注释**
无类型 λ 演算保证名称表达式(变量)和函数( λ 抽象)的规约的强规范化,但不保证函数应用规约的强规范化。
扩展的 λ 演算(如[简单类型 λ 演算](https://zh.wikipedia.org/zh-cn/%E7%AE%80%E5%8D%95%E7%B1%BB%E5%9E%8B%CE%BB%E6%BC%94%E7%AE%97))可保证规约函数应用的强规范化。
### 名称表达式
*名称表达式(name expression)* 是表示变量的 λ 项。
[原子表达式](#原子表达式)的由派生实现定义的非空子集是名称表达式。其它作为名称表达式的表达式[语法形式](#语法形式)由派生实现定义。
名称表达式不被进一步规约;其求值是[值替换规则](#求值规约)的平凡形式。
### 函数
*函数(function)* 是一种参与特定规约规则的实体,也可以指求值为函数实体的表达式。
一般地,函数表达式在 [WHNF](#规范化中间表示) 下作为[操作符](#规范化中间表示)被求值,其[最终求值结果](#范式)为函数实体,或函数对象(若函数在语言中允许作为对象)。
NPL 中,作为[一等对象](#基本语义概念)的函数表达式的最终求值结果是[合并子](#合并子)。
一个函数表达式是以下两种表达式之一:
* 保持[等价地](#求值性质)求值到其它函数表达式上的[名称表达式](#名称表达式),称为*具名函数表达式(named function expression)* ,简称*具名函数(named function)* 。
* 满足本节以下规则的由派生实现定义的*匿名函数表达式(anonymous function expression)* ,简称*匿名函数(anonymous function)* 。
函数应确定替换重写规则[被替换的目标](#替换策略),即*函数体(function body)* 。
除非派生实现另行指定,函数不需要被进一步规约,此时其求值是[值替换规则](#求值规约)的平凡形式。
在[类型系统](#类型系统和类型机制)中,函数可被指派*函数类型(function type)* 。函数类型能蕴含[参数](#函数内部的变量)和结果的[类型](#类型)。
**注释** 例如,在简单类型 λ 演算中,函数类型是[类型构造器](#类型) `→` 组合输入(参数)和结果(输出)类型的[复合类型](#类型)。
#### 函数内部的变量
匿名函数可以显式指定(*绑定(bind)* )包含若干变量使之成为[约束变量](#基本语义概念)的语法构造。
通过创建函数时的显式的语法构造引入的这种变量称为函数的*形式参数(formal parameter, parameter)* 。
除绑定外,匿名函数蕴含[上下文](#项重写系统)可以[捕获](#项重写系统)若干在函数体以外的同名的[自由变量](#基本语义概念)。
通过绑定或捕获引入的变量允许在函数体中允许使用。
使用[词法作用域](#实现环境提供的求值环境)时,若匿名函数所在[作用域](#基本语义概念)的存在同名的[名称](#名称),则被捕获的名称被[隐藏](#基本语义概念)。形式参数隐藏被捕获的[变量名](#npla-环境)。
派生实现的语义规则应满足和 λ 演算的语义的 α-转换(alpha-conversion) 规则不矛盾。
**注释** [Vau 演算](#vau-抽象)在没有限定[环境](#求值环境)时不考虑一般意义上的自由变量。
函数应用的求值决定被绑定的变量和函数体内的变量之间的关系,参见[函数合并](#函数合并)。此时,[求值策略](#求值策略)蕴含的[替换策略](#求值策略)蕴含被绑定的变量和函数体内的变量之间的[同一性](#基本语义概念)。
类似地,在被捕获的变量到函数体内捕获的变量之间,也有和替换策略一一对应的不同捕获策略。
除非另行指定,变量被*按引用捕获(captured by reference)* 而非*按值的副本捕获(captured by value copy)* ,即通过捕获引入的变量是被捕获变量的引用而不是副本。
**原理**
捕获为引用而不是副本,保持被捕获的变量和函数体内同名变量的同一性,在实体是对象时不影响可观察行为。若这些捕获未被使用,可被实现直接移除。
#### 过程
*过程(procedure)* 是操作符[具现](../Terminology.zh-CN.md#程序设计语言)的*可调用(callable)* 的实体,决定特定的可提供求值的作用(包括决定求值结果)的[计算](../Terminology.zh-CN.md#计算机科学)。
函数表达式的[最终求值结果](#范式)由过程实体的作用中的[结果](#基本语义概念)决定,以派生实现定义的方式关联。
通过函数表达式可指定可选的[实际参数](#函数合并),发生过程*调用(call)* 。过程的调用蕴含计算。
过程中和过程外的计算的组合满足[因果性](#规约顺序):
* 以求值描述的过程中的作用整体非[后序](#规约顺序)于引起过程中作用的外部环境的计算。
* 以求值描述的过程中的任意作用非后序于取得对应结果的[值计算](#基本语义概念),即结果是决定值的作用的[依赖](#规约顺序)。
*主调函数(caller function)* 等*调用者(caller)* 或其它引起过程中的计算的实体[转移](#基本语义概念)计算蕴含的[控制](#基本语义概念)到过程中的计算而使之*进入(enter)* 到*被调用者(callee)* 的过程。
过程可能被限制只有*一次(one-shot)* 调用有效;其它过程是*多次(multi-shot)* 的。
多次过程调用时控制可能通过调用被再次转移,即*嵌套调用(nested call)* 。
一些被多次调用的过程可能被多次进入,即*重入(reenter)* 。
一个调用中的重入相同或不同过程的次数称为调用的*深度(depth)* 。
推论:嵌套调用是深度大于 1 的调用。
通过嵌套调用直接(总是以自身作为调用者)或间接(通过其它调用者转移控制)的重入是*递归调用(recursive call)* 。
过程可以*返回(return)* 取得计算的值并可同时改变[控制状态](#基本语义概念),影响之后的计算。
**原理**
一次过程,特别是在其内部涉及和[续延](#续延)或[闭包](#实现环境提供的求值环境)的实现交互时,相对多次过程可能具有因其对持有资源的要求较宽松,而具有较小的性能开销。
**注释**
对象语言中的过程在描述操作语义的[元语言](../Terminology.zh-CN.md#程序设计语言)中可表示为函数,其应用可对应对象语言中过程的隐式调用。
违反一次过程调用有效地约束的程序典型地[引起错误](#错误)。
注意过程不一定可作为可被对象语言直接表达的*一等(first-class)* 函数,但同时在元语言中仍然可能可行。如[无界续延](#续延),因为可能不符合[函数的类型要求](#函数),详见[续延的捕获和调用](#续延的捕获和调用)中的原理。
一次重入的过程调用分配的资源对应一个[活动记录帧](#活动记录)。
#### 过程调用的计算顺序
按计算的顺序约束和默认返回控制的方式,可能有不同的形式。
*例程(routine)* 的求值不*交叉(interleave)* ,即例程中的计算和例程外的计算[非决定性有序](#规约顺序)。
**注释** 典型地,例程中的计算通过例程作为函数实体创建时的函数体确定。
作为不同的例程,不考虑例程中的计算的[续延](#续延)被保存时:
* *子例程(subroutine)* 在返回一次后不[重入](#过程)。
* *协程(coroutine)* 则可能被多次重入并引起多次返回。
和子例程的[正常控制](#程序的控制执行条件)不同,即便其中的计算不涉及显式地改变控制状态,协程可能蕴含控制从协程中的计算到协程外的计算的[转移](#过程):
* 引起多次返回对应改变[控制作用](#基本语义概念)。
* 转移控制后,函数体中的计算被*暂停(suspended)* 。
* 重入的协程可*恢复(resume)* 被暂停的计算。
* 不排除可被重入的协程作为函数实体,是*可恢复函数(resumable function)* 。
* 可被暂停和恢复的计算是*异步的(asynchrnous)* 。这和正常控制的*同步的(synchronous)* 的计算相对。
一般的续延支持返回多次并可能支持和调用者并发的计算,包括异步的计算;而协程蕴含的控制作用的改变对应不同续延的替换,也能实现类似的支持。
语言的语法可显式指定例程创建协程,也可以当前的控制状态创建和现有的例程没有直接对应的协程。后者类似[续延捕获](#续延的捕获和调用)。
NPL 支持函数求值得到过程。对象语言中的过程可能支持使用这些形式的一种或多种,具体形式由派生实现指定。
协程可能限制转移向下一步骤的计算转移的方向,即调用者和被调用者被通过创建其的语法构造确定,而不能在之后改变。
根据是否只提供一种不区分转移方向的原语,协程分为*对称(symmetric)* 和*非对称(asymmetric)* 协程:
* 对称协程转移控制到另一个协程,不需要单独区分不同的操作。
* 转移控制的源和目标之间没有[调用者](#过程)和[被调用者](#过程)的相对关系。
* 非对称协程对控制的转移分为*调用(invoke)* 和*出让(yield)* 操作,其中:
* 调用操作从调用者转移控制到被调用者,恢复之前保存的上下文(若有)或创建时的初始上下文。
* 出让操作暂停和保存当前上下文并返回(转移)控制到它的调用者。
* 一般地,转移控制的具体时机未指定,可蕴含(对应续延的)[调度](#续延)和[并发](../Terminology.zh-CN.md#计算机科学)执行。
* 一些协程称为*半(semi)* 异步协程(半协程),以体现实现异步计算的控制转移形势受限的非典型性。对应地,没有此类限制的协程被称为*全(full)* 异步协程(全协程)。
* 通常半协程指对控制的转移(相对传统的例程调用)受限,不能仅通过调用而需要单独的出让操作实现计算的暂停。这是非对称协程的同义词。
* 但半协程也可能指特指暂停在特定上下文受限的协程实现。
* **注释** 另见[这里](http://www.lua.org/pil/9.1.html)的说明。
根据是否协程持有[活动记录帧](#活动记录),协程分为*有栈(stackful)* 和*无栈(stackless)* 的。
* 两者提供不同的资源[所有权](#所有权抽象),而可能影响使用这些特性的程序中的资源的[可用性](#可用性)。
* 特别地,无栈协程不保证活动记录的可用性,无法直接支持创建的协程作为一等对象使用。
因为具有类似的改变控制的能力,有栈的、可作为[一等对象](#基本语义概念)的*全协程(full coroutine)* 可替代[一等续延](#续延的捕获和调用)。
**原理**
协程可视为是在计算上[可表达性](#完整性)等价的[一次续延](#续延的捕获和调用):
* 参见[这里](http://www.cs.tufts.edu/comp/250RTS/archive/roberto-ierusalimschy/revisiting-coroutines.pdf)。
* 其中,对称协程类似一次[无界续延](#续延),非对称协程类似一次[有界续延](#续延)。
* 对称协程可通过非对称协程补充操作实现。
* **注释** 一个这种设计的例子参见 [\[WG21 P0913R0\]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0913r0.html) 。
* 类似地,有界续延可[通过添加显式的界限实现无界续延](#续延)。
* 但是,这种功能相似不表示一一对应。
* **注释** 参见以下关于出让操作使用续延实现的讨论。
* 在核心语言支持*存储(store)* 即[可修改](#实体的不可变性)的[一等状态](#一等状态)的[副作用](#基本语义概念)的前提下,非对称协程和对称协程在可表达性上等价。
* 有界续延可不依赖其它副作用表达状态,但[无界续延无法表达](#一等状态)。
* 一等续延和协程在一定条件下可互相实现。
* 对称协程可实现一次续延。
* **注释** 另见[这里](http://www.inf.puc-rio.br/~roberto/docs/MCC15-04.pdf),但这个实现没有检查续延调用内部可能的非预期重入,且不满足 \[Hi90] 中的 [PTC 要求](#尾调用和-ptc)。
* 因此,作为一等对象时,协程可作为一等续延的替代实现方式。
* 非对称协程的出让操作可通过无界续延实现。
* 虽然可实现出让操作的[续延捕获](#续延的捕获和调用)并非[续延调用](#续延的捕获和调用),但续延调用对控制的转移不必然蕴含区分调用和调用者。
* 事实上,使用 `call/cc` 捕获续延创建的是无界续延。
* 有界续延可实现无界续延,因此出让操作也可使用有界续延实现。
* **注释** 另见[这里](https://cs.indiana.edu/~sabry/papers/yield.pdf)。
典型的设计中,函数表达式默认创建例程,而协程使用特设的语法标记过程得到。特设的关键字(如 `yield` )提供语法,对应非对称协程中的出让操作。
**注释**
关于过程的参数和过程调用之间的计算顺序,参见[求值策略](#求值策略)。
#### λ 抽象
λ 抽象(lambda abstraction) 是 λ 演算中的基本构成之一,提供匿名函数。
**注释** λ 抽象的语法包含的形式是典型的操作符。
在原始的无类型 λ 演算中,λ 抽象[不支持蕴含副作用](#求值性质),子表达式求值顺序任取而不改变[范式](#范式)的存在性和值。
在使用[热情求值](#λ-求值策略)的语言中,λ 抽象创建的过程是应用[合并子](#合并子)。
#### vau 抽象
Vau 抽象(vau abstraction) 是 vau 演算 (\[Shu10]) 中的基本构成之一。
Vau 抽象创建的过程是操作[合并子](#合并子)。
**注释** 使用 vau 抽象可实现引入 λ 抽象的操作符,如 \[RnRK] 提供的 `$vau` 操作合并子。
### 函数合并
具有操作符和操作数的项的组合可被特定的方式进行规约。这种组合是*函数合并(function combination)* ,包含:
* 具有至少一个约定位置的[子项](#基本语义概念) `E1` 的[复合表达式](#复合表达式) `E` ,当且仅当 `E1` 是被求值作为[操作符](#规范化中间表示)的函数时,`E` 是*函数合并表达式(function combination expression)* 。
* 其它具有操作符和操作数的项是非表达式形式的函数合并。以下操作符和操作数记作 `E1` 和 `E2` 。
以下规则中,非表达式形式的函数合并也可被视为表达式求值。
求值函数合并时,子项 `E1` 总是被求值。
除 `E1` 外表达式的剩余子项 `E2` 是[操作数](#规范化中间表示),在 E 被求值时以操作数决定的值等效*替换(substitute)* 函数的[形式参数](#函数内部的变量)。
替换形式参数的值是*实际参数(actual argument, argument)* 。
函数合并的求值是[值替换规则](#求值规约)的非平凡形式。
若替换操作数 `E2` 在合并中被求值,函数合并 E 是函数应用表达式,简称*函数应用(function application)* 。
若操作符是 λ 抽象,`E2` 视为一个整体,则函数应用替换规则对应 λ 演算的 β-规约(beta-reduction) 规则。
其它函数合并使用的替换规则由派生实现指定。
派生实现应指定函数合并[规约](#基本语义概念)的结果是[规范形式](#范式),它对应的值是函数合并的[求值结果](#基本语义概念)替换被求值的表达式的实体,称为*函数值(function value)* 。
函数应用匹配实际参数和对应的引入形式参数的构造。匹配可能失败。确定匹配参数成功的条件是等价关系,称为参数匹配一致性,由参数匹配的等价关系指定。
匹配成功的每个实际参数和被匹配的目标(可能是形式参数)具有一对一或多对一的对应关系。
伴随参数匹配,实现可引入其它必要的操作(如为匹配分配资源和确定上述对应关系)。这些操作可具有和确定参数对应关系的匹配之间[非决定性有序](#规约顺序)的副作用。
仅当上述必要操作及所有实际参数的匹配成功,替换 `E1` 决定的某个关联表达式中和形式参数结构一致的子表达式为实际参数。替换参数的结构一致性是等价关系。
表达式相等蕴含参数匹配一致性和替换结构一致性。实现可分别定义其它规则扩充这些等价关系的外延。
替换参数的值蕴含对实际参数的计算的依赖,即参数若被求值,其[值计算](#基本语义概念)[先序](#规约顺序)函数应用的求值;但其它求值顺序没有保证。
**注释**
一般地,根据 `E1` 的值,操作数或操作数的值计算的结果被作为实际参数。
[过程](#过程)及其调用在其操作语义的[元语言](../Terminology.zh-CN.md#程序设计语言)中通常表达为函数及函数合并。
若过程的结果被忽略,则通常表达为[单元类型](#类型)的值。
此外,一些语言中忽略过程的结果是[空类型](#类型),以检查错误的使用。NPL 不要求语言具有[静态类型](#类型系统和类型机制)规则,也不要求这些检查。
#### 函数调用
求值函数合并包含子表达式的求值:总是求值操作符,并可能求值操作数。若这些求值都没有退出,则发生函数*调用(call)* ,函数是*被调函数(called function)* 。
若被调函数存在形式参数,函数调用首先以操作数的直接子表达式作为实际参数,匹配实际参数和形式参数。
若实际参数匹配的目标可指定一个变量,则伴随参数匹配的操作包括以特定规则[绑定](#函数内部的变量)的形式参数。
绑定的实际参数和对应的形式参数作为不同的实体时,作为伴随参数匹配的必要操作的一部分,发生*参数传递(parameter passing)* 。参数传递使形式参数具有作为实际参数值的副本。参数传递可能使和实际参数相关的资源被复制或转移。
实现在函数合并的求值中应提供函数调用的支持。
函数调用确定副作用的边界:保证参数表达式在函数应用被求值之前被求值。
在控制[返回](#过程)时,函数调用内部确定的值最终替换被求值的函数合并而作为函数值,即为*返回值(return value)* 。
若函数是[过程](#过程),对应的函数调用是*过程调用(procedure call)* 。
若一个函数的调用仍待返回,则该函数调用是*活动的(active)* 。
调用总是不蕴含非[纯求值](#求值性质)的函数是*纯函数(pure function)* 。
函数调用的中蕴含的求值对应的[规约步骤](#规约规则和求值)的集合是它的*动态范围(dynamic extent)* 。
函数中被[捕获](#函数内部的变量)的实体的引用和求值函数中的计算创建的对象的引用构成函数计算的结果时,引用可能*逃逸(escape)* ,即在调用的动态范围以外可访问。
派生实现可能约定附加的名义特征区分其它情形相同的调用,称为*调用约定(calling convention)* 。
**注释**
典型实现的函数指称过程,函数调用是过程调用。
一般地,被调用的函数及函数调用的作用的等价性通常不能被确定。
一个重要的子类是[不能确定具体表示](#规范化中间表示)的情形,参见[合并子](#合并子)。其它函数一般也有类似限制。
关于函数调用中的求值,另见函数调用的[终止保证](#终止保证)。
和 \[RnRS] 不同,动态范围仅对求值定义,而不是关于环境中的绑定显示[计算作用](../Terminology.zh-CN.md#程序设计语言)的属性。这种属性事实上对象的[生存期](#基本语义概念),仅对对象而非更一般的实体有效。
[续延](#续延)可用其动态范围表示。
本文档的动态范围的概念定义和 \[RnRK] §7.1 的定义兼容,但不依赖其对续延的描述,也适用[抽象机语义](#实现行为),是 \[RnRK] 的一般化。
\[Racket] 使用求值的规约步骤在表达式上定义动态范围。NPL 不在表达式上采用类似的定义,因为:
* 类似 \[RnRK] ,NPL 强调支持对象语言中的[显式求值风格](#其它设计和实现参考)及表达式求值前后的不同。
* 类似 \[RnRK] ,进一步地,NPL 派生语言(如 NPLA1 )可明确支持在对象语言中指定[求值环境](#求值环境)而改变求值的上下文,表达式不能被预期通常以[上下文](#上下文相关求值)无关的方式被求值。
调用约定可提升实现细节,为[互操作](../Terminology.zh-CN.md#规范)提供接口保证,避免非预期的不兼容实现的混合。
#### 合并子
除非另行指定,NPL 假定函数合并满足以下典型情形,即函数合并的[操作符](#规范化中间表示)求值为以下类型的*合并子(combiner)* 之一:
* 对操作数的直接[操作](#规范化中间表示)(而不要求对操作数求值)的合并子是*操作合并子(operative combiner)* ,简称*操作子(operative)* 。
* 进行函数应用的合并子是*应用合并子(applicative combiner)* ,简称*应用子(applicative)* 。
* 由派生实现定义的*扩展合并子(extended combiner)* 。
合并子的函数应用(依赖对操作数进行至少一次求值)是*合并子应用(combiner application)* 。
合并子应用使用[应用序](#求值策略)。
应用子总是对应一个*底层(underlying)* 合并子,可通过底层合并子上的一元的*包装(wrap)* 操作得到;其逆操作为*解包装(unwrap)* 。
解包装结果不是扩展合并子的合并子称为*真合并子(proper combiner)* 。
合并子上可以定义若干等价关系,这些等价关系蕴含关于函数应用替换的基本形式:
若对任意上下文,替换一个应用中的合并子为另一个不改变函数应用替换的结果,则这两个合并子等价(对应 λ 演算的 β-等价)。
**注释**
合并子被[调用](#函数调用)时通常[返回](#过程)且仅返回一次。
**注释** 详见[续延的捕获和调用](#续延的捕获和调用)。
由于程序可能引入未知具体表示的合并子(如从其它模块链接),以上等价可能无法判定,不要求实现提供。
因为本设计不依赖 λ 抽象的[内部表示](../Terminology.zh-CN.md#程序设计语言)(特别是支持[惰性求值](#λ-求值策略)为目的的),不依赖 η-变换的[可用性](#可用性),也不要求支持更强的 βη-等价。
派生实现可按需定义较弱的[等价谓词](#实体的等价性),保证其判定结果蕴含上述等价关系的结果。
#### 续延的捕获和调用
语言可提供作为[一等实体](#基本语义概念)的[续延](#续延)即*一等续延(first-class continuation)* 。
续延的*捕获(capture)* [具现](../Terminology.zh-CN.md#程序设计语言)[当前续延](#续延)为对象语言中可操作的一等续延。
类似[过程](#过程),续延可被[一次或多次调用](#过程),称为*续延调用(continuation call)* 。
续延调用接受一个[实际参数](#函数合并)作为传递给后继[规约步骤](#规约规则和求值)使用的[值](#基本语义概念)。除非另行指定,续延参数被[按值传递](#求值策略)。被调用的续延可[访问](#基本语义概念)参数并执行其蕴含的其余规约步骤。
和接受实际参数对应,续延可被假定关联一个等效的[应用子](#合并子),具有一个[形式参数](#函数内部的变量),这个应用子的[底层合并子](#合并子)被[调用](#函数调用)时[非正常地](#程序的控制执行条件)传递它的[操作数](#规范化中间表示)给关联的续延。
对象语言可支持符合[函数类型要求](#函数)的一等续延作为函数。作为一等续延的函数可直接作为[合并子](#合并子)构成[函数合并](#函数合并)进行[函数调用](#函数调用),而实现续延调用。
除非派生实现另行指定,NPL 的一等续延不是函数。
函数应用(如[合并子调用](#函数调用))可隐含([非一等对象](#一等实体和一等对象)的)续延调用。
续延调用的其它的具体形式由派生实现定义。
除非在捕获的续延上存在特定的[控制作用](#基本语义概念),合并子被调用时以当前续延[返回](#函数调用)且仅返回一次。
类似[函数应用表达式](#函数合并),*续延应用(continuation application)* 表达式是求值时蕴含[续延调用](#续延的捕获和调用)的表达式。
**原理**
在 Scheme 中,一等续延即过程。
在限制[元语言](../Terminology.zh-CN.md#程序设计语言)的函数不蕴含控制作用时,类似 Scheme 等支持的无界[续延](#续延)不是函数。一个理由不能以常规方式为无界续延指定是函数[类型](#类型)。参见[这里](https://okmij.org/ftp/continuations/undelimited.html#introduction)的介绍。
在 Kernel 和其它一些语言中,续延不是过程,而具有不同的[名义类型](#类型等价性)。这种不同于 Scheme 的设计是有意的。
NPL 一等续延不限制是否和函数类型同一,因此无界续延仍可被视为函数(或更确切地,即程序入口作为边界的有界续延)。
类似 \[RnRK] 的设计,因为一等续延的调用可引起和更常见的过程调用显著不同的控制作用,续延调用有必要和过程调用在对象语言的语法上显式区分以满足[易预测性](#易预测性),因此一等续延一般不是函数。
续延关联的等效应用子的原理同 \[RnRK] §7 和 §7.2.5(应用子 `continuation->applicative`)的原理,但略有不同:
* 作为一等对象的续延和续延的实际参数是否求值无关,因此不是合并子,[求值算法](#求值规约)不需要支持续延作为函数合并被求值;但续延可通过特定的操作转换为应用子。
* 续延和操作子在被调用时都接受一个实际参数对象。
* 对前者,对象典型地表示计算的结果,即已被求值。
* 对后者,对象是操作数。典型地,操作子作为应用子的底层合并子,操作数已被作为应用子的实际参数被求值算法求值。
* 类似 \[RnRK] 而和 \[RnRS] 不同,因为[函数合并](#函数合并)可接受[非真列表](#广义列表)作为参数,非列表的操作数可以和非列表的续延实际参数直接对应。
* 因为函数合并的这种性质,续延关联的应用子的应用和续延应用存在直接的一一对应关系。
* 但是,为避免和[简单性](#简单性)冲突,\[RnRK] 的*选择器(selector)* 支持在此未被要求。
**注释**
类似过程,续延及其调用在其操作语义的元语言中能表示为元语言的[函数应用](#函数合并),通常表达为[函数](#函数)及[函数合并](#函数合并)。
续延捕获在语法上类似函数对变量的[捕获](#函数内部的变量)。被捕获的实体通常以引用保存。被捕获的实体通常是隐式的,即不在对象语言程序中出现。
在支持一等续延且捕获的续延可被复制的语言中,实现需要考虑活动记录的复制,参见 \[Hi90] 。
关于控制作用,另见续延调用[对程序控制的改变](#程序的控制执行条件)。
#### 活动记录
[活动的](#函数调用)合并子分配的对象称为*活动记录(activation record)* 。
[函数调用](#函数调用)以活动记录引用涉及的变量。每个调用的活动记录中可保存多个变量。活动记录可能因此持有状态,即便不一定可被函数调用外的操作直接修改。
嵌套的函数调用具有多次分配的活动记录。为强调其中的对应关系,每一个调用关联其中的一个*帧(frame)* 。
在确定一次分配的一个活动记录对应一次函数调用的实现中,一个活动记录和一个活动记录的帧同义。
活动记录的集合可能构成特定的数据结构。例如限制只支持嵌套的子例程调用(而不支持一般的[续延调用](#续延的捕获和调用))时,具有后入先出(LIFO, last-in-first-out) 的栈的结构。
### λ 求值策略
在[变量](#名称表达式)绑定值后,兼容 λ 演算规约语义(特别地,β-规约)的表达式的[具体求值](#基本语义概念)根据是否传递操作数对使用[按需传递](#求值策略)的情形分为三类:
* (完全)*惰性求值(lazy evaluation)*
* 部分惰性求值
* *热情求值(eager evaluation)*
其中,惰性求值总是使用按需传递,热情求值总是不使用按需传递,部分惰性求值不总是使用或不适用按需传递。
在保证不存在非[纯求值](#求值性质)时这些求值的[计算作用](../Terminology.zh-CN.md#程序设计语言)没有实质差异。存在非纯求值时,使用的 λ 求值策略由派生实现定义。
[非严格求值](#严格性)严格蕴含惰性求值。两者经常但不总是一致,例如,实现可能[并行地](../Terminology.zh-CN.md#计算机科学)热情求值,并舍弃部分结果以实现非严格求值。
热情求值蕴含[严格求值](#严格性)。两者也经常但不总是一致,例如,实现可能使用应用序严格求值。但因为非严格的热情求值缺乏性能等可局部优化的实用动机,这种不一致的情况通常不作为附加的语言特性提供(而仅为简化实现默认作为全局策略使用)。
**注释**
由于实现可能确定特定表达式的作用对约定必须保持的[程序行为](#状态和行为)没有影响而可能省略求值,按[抽象机](#实现行为)语义的严格求值在实际实现中通常是不必要的。
惰性求值可通过中间值[延迟求值](#严格性)实现。
## 上下文
*上下文(context)* 是表达式关联的状态的特定集合。
**注释** 这里不是[自指概念](../Terminology.zh-CN.md#自指)。
一个上下文是*显式的(explicit)* ,当且仅当它可以通过[名称表达式](#名称表达式)访问。
一个上下文是*隐式的(implicit)* ,当且仅当它不是显式的。
隐式的上下文通常是[管理状态](#状态和行为)。
确定上下文的状态或对可变上下文的[修改](#基本语义概念)是对上下文的[访问](#基本语义概念)。
规约规则中,以未指定子项参数化的项是一个上下文。
本节以外其它关于上下文的具体规则由派生实现定义。
**注释**
参数化的子项可在([元语言](../Terminology.zh-CN.md#程序设计语言)的)语法上被表示为一个*洞(hole)* ,详见[上下文相关求值](#上下文相关求值)中的语法 `[]` 。
[过程实体](#过程)能影响[函数表达式](#表达式)关联的上下文,参见[函数和函数应用的求值环境](#函数和函数应用的求值环境)。
### 求值环境
*求值环境(evaluation environment)* 是在求值时可访问的隐式上下文,提供可通过[名称解析](#名称解析)访问的变量的[绑定](#基本语义概念)。
不和[实现环境](#略称)相混淆的情况下,求值环境简称(变量或对应的局部绑定所在的)为*环境(environment)* 。
具有[可见名称](#可见名称)的绑定是*可见的(visible)* 。
环境*包含(contain)* 若干个*局部绑定(local binding)* ,即不通过其它环境即保证可见的*被绑定实体(bound entity)* 。
环境*展示(exhibit)* 可见的绑定。
一个环境是*空环境(empty environment)* ,当且仅当其中包含的局部绑定集合是空集。
**注释**
按绑定的定义,求值环境的局部绑定集合即变量的名称和通过声明引入的被变量表示的实体构成的映射。
可见绑定可能被通过名称解析成功访问变量。
包含和展示的定义同 \[RnRK] 。除此之外,[环境对象具有直接包含的绑定的所有权](#环境对象)。
#### 实现环境提供的求值环境
实现环境可能在实现以外提供附加的求值环境作为任务通信的机制,如环境变量。
除非派生实现另行指定,语言支持的求值环境和这些机制蕴含的求值环境的交集为空。语言可以库的形式提供 [API](../Terminology.zh-CN.md#程序设计语言) 另行支持。
#### 函数和函数应用的求值环境
在典型的对象语言中 [λ 抽象](#λ-抽象)中指定的替换构造具有*局部作用域(local scoping)* ,其中可访问 λ 抽象外部词法意义上*包含的(enclosing)* 求值环境的变量,对应求值环境为*局部环境(local environment)* 。
在基于*词法作用域(lexical scoping)* 的对象语言中,引入 λ 抽象对应的语言构造支持[捕获](#函数内部的变量)引入函数时所在的作用域的环境,称为*静态环境(static environment)* 。
相对地,*动态作用域(dynamic scoping)* 根据求值时的状态指定指称。
[Vau 抽象](#vau-抽象)进一步支持在局部环境中提供访问[函数应用](#函数合并)时的求值环境,即*动态环境(dynamic environment)* 的机制。
除非另行指定,按*词法闭包(lexical closure)* 规则捕获,即只根据词法作用域确定捕获的指称;若需要支持依赖求值状态动态确定指称时,使用派生实现提供的对求值环境的操作,而不依赖动态作用域。
作为[过程](#过程)的实现,词法闭包规则捕获实体创建*闭包(closure)* 。
除非另行指定,NPL 只存在一种作用域,即所有作用域都使用相同的[名称解析](#名称解析)和捕获规则。
**注释**
历史上,闭包首先在 [SECD 抽象机](https://academic.oup.com/comjnl/article-pdf/6/4/308/1067901/6-4-308.pdf)中引入。术语闭包来自 λ 演算的[闭项](#项重写系统)。
### 互操作上下文
用于[互操作](../Terminology.zh-CN.md#规范)的和求值关联的隐式上下文是*互操作上下文(interoperation context)* 。
除非派生实现另行指定,语言不提供访问互操作上下文的公开接口。
**注释**
一个典型的实例:由 [ISA](#补充领域定义)约定的通用架构寄存器的状态,可能需要在[函数调用](#函数调用)或任务切换过程中保存和重置。
## 类型
*类型(type)* 是上下文中和特定的实体直接关联或间接关联的元素,满足[某个执行阶段的不变量约束](#阶段不变量约束)。
*类型规则(type rule)* 是和类型相关的对象语言的语义规则。
实体关联的类型可能被显式地指定,或通过隐式的限定规则推断确定。符合指定和限定要求的类型可有任意多个。
实体的类型是被显式指定的实体关联的类型。实体具有实体的类型以及通过其它规则限定的类型。实体是类型的*实例(instance)* 。
类型可用集合[表示](#表示)。集合的元素是具有其表示的类型的实体。
表示类型的集合为空时,表示类型没有实例,是*空类型(empty type)* 。
推论:由集合的形式表达,空类型是唯一的。
表示类型的集合只有一个元素时,类型只有一个不可区分的实例,这样的类型是*单元类型(unit type)* 。
和表达式直接关联的类型满足起始阶段不变量约束,称为*静态类型(static type)* 。
和表达式的[值](#基本语义概念)关联的类型满足[运行阶段](#实现的执行阶段)的不变量约束,称为*动态类型(dynamic type)* 。
其它可能存在类型或实现执行阶段的扩展由派生实现定义。
除非另行指定,对象的类型是[对象的值](#基本语义概念)的类型。
NPL 对象类型和[存储的值](#表示)的类型之间的关联未指定。
类型在描述类型规则的[元语言](../Terminology.zh-CN.md#程序设计语言)中可作为对象。
生成对象的元语言函数是*类型构造器(type constructor)* 。类型构造器的参数是类型,的函数值是组合这些参数得到的*复合类型(compound type)* 。
### 类型系统和类型机制
称为类型的具体实体和之间的关联由派生实现的*类型系统(type system)* 规则指定。
默认类型系统不附加约束,所有表达式或关联的项都*没有指定类型(untyped)* ,为退化的*平凡类型系统(trivial type system)* 或*单一类型系统(unityped system)* ,实质上是动态类型。
对类型系统的分类中,类型也指确定类型的过程称为*类型机制(typing discipline)* ,其中确定类型的过程称为*定型(typing)* 。
在静态类型之后阶段确定的类型机制是*动态定型(dynamic typing)* 。
除非另行指定,被确定的静态类型的阶段是翻译时阶段;被确定的动态类型的阶段是翻译时之后,即[运行时](#实现的执行阶段)。
语言可提供[定型规则(typing rule) (en-US)](https://en.wikipedia.org/wiki/Typing_rule) ,指定[项](#基本语义概念)作为实体在特定的[上下文](#上下文)(称为*类型环境(typing environment)* )中的类型。项是类型在这个上下文中的*居留(inhabitant)* 。
类型环境确定*类型指派(type assignment)* ,即项和类型的之间的*定型关系(typing relation)* 。定型确定的这种定型关系的[实例](../Terminology.zh-CN.md#程序设计语言)即*定型判断(typing judgement)* 。
不违反类型系统规则下的[良定义的](../Terminology.zh-CN.md#规范)程序构造是*良型的(well-typed)* 。
根据是否要求项首先都是良型的再指派语义,带有类型的形式系统可具有[*内在(intrinsic)* 和*外在(extrinsic)* 的解释](https://en.wikipedia.org/wiki/Simply_typed_lambda_calculus#Intrinsic_vs._extrinsic_interpretations)。
除非另行指定,NPL 使用外在的解释。
**原理**
默认使用外在解释的理由是:
* 类型的外在解释允许在一个没有指定具体类型系统设计的单一类型系统为基础扩展不同的类型系统,能满足语言自身[可扩展](#变化的自由)的需要。
* 扩展通用目的语言特性的顺序应是从简单到复杂的,而不是相反,因为并不存在已知的万能语言可供裁剪。
* 这也符合历史顺序:无类型 λ 演算被扩展到不同的有类型 λ 演算,而不是相反;因为有类型 λ 演算的规则明显较无类型 λ 演算多且复杂。
* 从无类型 λ 演算可以扩展到的一些特性更丰富其它系统,如 [λμ 演算 (en-US)](https://en.wikipedia.org/wiki/Lambda-mu_calculus) 和 vau 演算,首先都是无类型的,并不存在可用的内在解释。
* 为了描述类型规则,外在解释最终需要在整个系统中引入和对象语言不同的[元语言](../Terminology.zh-CN.md#程序设计语言),而增加复杂性。
* 即便存在强调可扩展的对象语言(如 [MLPolyR](https://arxiv.org/abs/0910.2654)),至少语言规范中定义的元语言没有被证明可以和被描述类型规则的对象语言合并。
* 即便能证明可以合并,这种方式也显著地大大增加了设计的复杂性,违反[避免不必要付出的代价](#避免不必要付出的代价)。
* 根本上,这种方式损害对象语言设计的[光滑性](#统一性),很可能大大削弱对象语言的[可用性](#可用性)。
* 没有确切的充分依据证明引入类型系统带来的性质是通过非类型论的直接扩展演绎系统的方式不能实现或者其实现有现实困难的。
* 因此先验地要求类型的存在缺乏必要性。即便可实现需求,在通用目的上通常是舍近求远的过度设计。
* 即便引入类型的方式有现成的工程实践而可以提升工程效率,也可能是[过早的优化](#避免不成熟的优化)。
* 更何况现实并没有证据表明存在这样的成功实践。
* 跳出先验地引入类型的做法,使用先验的内在解释而排除不够清晰明确的含义(meaning) 的语法的方式,在历史上存在更显著的失败。
* \[Chu41] §18 试图排除原始的 λ 演算(称为 λ-K-转换,在 \[Bare84]\[Shu10] 中称为 λ*K* 演算)中无法取得[范式](#范式)的项(以使之更适用于符号逻辑的目的):限制 λ 抽象中的约束变量是第二子项的自由变量。
* 非正式地,这在语法上要求每个[函数体](#函数)中的每个[变量](#基本语义概念)必须是某个唯一的函数的[形式参数](#函数内部的变量),且这个函数的函数体是语法上包含这个变量的表达式,即语言在语法上禁止出现(在声明以外)未使用的形式参数。
* 限制的 λ 演算在现代(如 \[Bare84]\[Shu10] )称为 λ*I* 演算,不支持表达 [K 组合子](https://zh.wikipedia.org/zh-cn/%E7%BB%84%E5%90%88%E5%AD%90%E9%80%BB%E8%BE%91)。
* \[Bare84] §2.2 指出 λ*I* 演算具有的一些问题,如:
* 对应的理论 **λ*I*** 翻译到[组合子逻辑](https://zh.wikipedia.org/zh-cn/%E7%BB%84%E5%90%88%E5%AD%90%E9%80%BB%E8%BE%91)的理论 **CL** 时,项能取得[范式](#范式)的性质不被保持。
* 范式的概念过于侧重语法,所以在[模型](../Terminology.zh-CN.md#计算机科学)中不确定含义。
* 试图识别编码[偏函数(partial function) (en-US)](https://en.wikipedia.org/wiki/Partial_function) 需要的“未定义”的项是不可能的。
* λ*I* 演算定义的偏函数的组合对应的项不一定是 λ*I* 演算定义的被组合的偏函数的项的组合。
* \[Bare84] §2.2 指出,这些问题都来自 \[Chu41] 选择用无法取得范式的项编码“未定义”的概念。
* 定义可解性(solvability) ,以不可解代替不能取得范式编码“未定义”可解决这个问题。
* 项的可解性定义为存在有限的项序列使前者在后者顺序应用得到 [I 组合子](https://zh.wikipedia.org/zh-cn/%E7%BB%84%E5%90%88%E5%AD%90%E9%80%BB%E8%BE%91)(即 λ*x*.*x* )。
* 在 λ*I* 演算中,不可解等价不能取得范式。而在 λ 演算中,不可解等价不能取得 [HNF](#规范化中间表示) 。
* λ 演算没有 λ*I* 演算的上述问题。
* λ*I* 表述的 Church–Turing 论题仅限于[全函数](#终止保证),而 λ 函数表述的论题能扩展到一般形式的偏函数。
* 即便不考虑上述整体性质,尽管计算上 λ*I* 演算是 [Turing 完备](#完整性)的,它不能编码[常量](#基本语义概念)函数。
* 因为也不包含[管理规约](#管理规约)规则,它实际上无法编码[可观察性质的项](#状态和行为),除非平凡地指定所有项都是可观察的。
* 以上问题一定程度上揭示了去除似乎冗余但实际在语义上可能非平凡的语法构造是[不成熟的简化](#避免不成熟的优化),损害系统的[可用性](#可用性)。
* 要求(可被类型检查的)类型系统直接排除不能取得范式的项,在这个意义上比 λ*I* 演算对去除特定的项的组合更彻底。
* 即便类型系统能引入其它语义,这以引入不能被对象语言表达的规则为代价,通常需要元语言。
* 相比之下,同样是引入对象语言表达式无法表达的语义,管理规约是对象语言规则能直接蕴含的,相对具有更小的(工作量和避免兼容问题上的)代价。
* **注释** 内在解释又被称为 Church 风格的。
* 哲学意义上,内在解释或本体论上的(ontological) 解释,相比外在解释或语义上的(semantical) 解释需要更强的假设。
* 本体论上的逻辑,如 [Frege-Church 本体论 (en-US)](https://en.wikipedia.org/wiki/Frege%E2%80%93Church_ontology),可能解决一些悖论。
* 但根本上,没有充分动机指出,不涉及[演绎规则](#演绎规则)的悖论必须在通用语言内部直接提供规则消除,而不能通过其它方式(例如,由[用户程序](#程序实现)补充前提)解决。
* 指定管理规约可以编码非平凡的表达语言规则外的语义的项可以对这样的前提建模并在语言中适当编码表达。
* 编码表达这种方式是 NPL 强调 [N(name)](#词法规则) 和其它实体分离的主要理由。
* 本体论假设要求名称以外附加实体以使假设生效。一般地,这些假设以不同语言的陈述作为断言实现。这些陈述涉及特称对象时,在完备性上是可疑的,且容易和[开放世界假定](#开放性)冲突。
* 约定不涉及的语言规则的本体论假设在这些意义上也可被认为在效用上的[不成熟的优化](#避免不成熟的优化)。
**注释**
在元语言的意义上,类型系统包含语法和对应的语义,但在对象语言中,定型规则和其它推理规则(如[类型检查](#类型检查)规则)作为语言规则是语义规则,和语法相对独立。
实体的类型可被指定为未指定类型,以明确类型的存在性,但不明确具体的类型的构造和表示。
形式地,在类型系统中,类型环境和项作为前提,通过定型规则(typing rule) 得到定型判断。定型规则在逻辑上可以是公理或定理。
在数理逻辑中,使用结构主义数学方法,集合可以作为描述类型规则的理论(句子集合)的模型,和理论支持描述的类型一一对应。
### 类型等价性
通过显式指定标识(如名称)的方式定义类型的方法是*名义类型(nominal typing)* ,否则是*结构化类型(structrual typing)* 。
除非另行指定,不同的名义类型不蕴含等价关系。结构化类型之间的等价关系由实现定义。
类型的相等关系是一种类型之间的等价关系。两个类型相等,当且仅当它们的实例作为元素的两个集合对应相等。
除非另行指定,相等的类型不在语言中区分,且元语言(描述对象语言的规则)中类型作为实体的[同一性](#基本语义概念)即类型相等性。
推论:除非另行指定,不同的类型不等价。
除非另行指定,对象语言使用类型相等性实现类型等价性。
**原理**
对象语言中的类型实质上是类型的一种间接的表示,作为实体仍然可以具有不同的同一性。
这避免程序可能需要枚举类型的外延(即精确实现出表示它的集合)才能确保确切表示出这个类型这样的计算上不可行的困难。
因为可支持的表示的[类型全集](#类型全集)不同,类型相等是相对的,依赖类型系统的具体实现。一个类型系统可能支持无法在另一个类型系统中精确表示的类型。
**注释**
本节的主要例外参见[公共子类型](#类型序)。
### 类型标注
根据是否需要特定的文法元素指定和项关联的类型即*类型标注(type annotation)* ,对确定类型的机制可进行分类。
类型系统可使用*显式类型(explicit typing)* ,即在[定型](#类型系统和类型机制)时要求类型标注。
不使用类型标注的方式是*隐式类型(implicit typing)* 。
在引入实体(特别地,如[变量](#基本语义概念))时指定实体的显式类型标注称为*清单类型(manifest typing)* 。
不使用清单类型而使隐式引入的实体(如值)关联具体类型的机制称为*潜在类型(latent typing)* 。
清单类型是显式类型的实例;除此之外,显式类型还包括*铸型(casting)* ,即显式指定表达式求值的结果应具有的类型。
潜在类型是隐式类型的实例;除此之外,隐式类型还包括*类型推断(type interferece)* ,即通过隐含的上下文信息判断表达式关联的类型。
类型推断的逆过程是*类型擦除(type erasure)* 。类型擦除支持使一个[良型](#类型系统和类型机制)的程序中的已被定型的实体表示擦除前按类型规则不允许表示的其它实体。
若类型机制可保证在某个[执行阶段](#实现的执行阶段)内有确定[强规范化性质](#范式)的算法确定类型,则类型机制在该阶段是[静态定型](#类型系统和类型机制)。
**注释** 强规范化性质的算法保证终止。
语言可能个别指定引入这些类型相关的规则,在保持逻辑相容的前提下可混合使用。
显式类型可编码接口的要求,即*类型签名(type signature)* 。
类型签名通常直接指定[名义类型](#类型等价性),但同时也可允许非特定的满足结构类型约束的类型。这些类型和类型签名*兼容(compatible)* 。
**原理**
历史上,表达式的类型和变量的类型在[简单类型 λ 演算](#λ-完备语义和对应语法)中同时被引入。后者修饰 λ 抽象中的[自由变量](#基本语义概念),而前者限定剩余的所有项。
即便从[项重写系统](#基本语义概念)中两者是形式上统一的,在实际语用中具有很不同的差异。这集中体现在后者是[名义的](#类型等价性),除非附加其它不同的语法设施,并不具有结构化推导的性质,原则上只适合描述接口;而前者能兼容[结构化类型](#类型等价性),同时适合描述接口及其实现。
作为接口的名义类型在作为自由变量以外的上下文中重新复用为不关心其类型(并消除依赖这些信息的[其它机制](#类型检查))的其它程序构造(一般意义上的表达式),通常需要类型擦除等更复杂的机制和支持的类型系统规则,以消去不再预期和其它类型系统规则交互的类型。
和 \[RnRK] 类似,NPL 不要求使用清单类型,以避免一些一般意义上的全局设计缺陷。这些缺陷包括:
* 过于积极地(非预期地)排除危险但对程序有用的使用,而违反[易预测性](#易预测性)。
* 因为移除类型标注需要上述的复杂机制和类型系统规则,具体的清单类型阻碍派生语言定义其它不容易冲突的类型标注规则而使语言具有更好的[可扩展性](#变化的自由)。
* 因为名义类型的相关规则更容易直接拒绝一些和类型规则不兼容的程序构造而难以简单地变通,往往对程序构造的组合具有更多直接的[可表达性](#完整性)限制而破坏通用计算意义上的[正确性](#正确性)。
* **注释** 例如,许多类型系统不允许表达 [Y 组合子](https://zh.wikipedia.org/zh-cn/%E4%B8%8D%E5%8A%A8%E7%82%B9%E7%BB%84%E5%90%88%E5%AD%90#Y%E7%BB%84%E5%90%88%E5%AD%90)的构造[良型](#类型系统和类型机制)。
若有必要,派生语言仍可限定使用清单类型。一般仍然建议仅在局部引入而避免全局复杂性和因此带来的限制。
**注释**
类型签名来自数理逻辑术语。
### 类型检查
*类型检查(typechecking)* 解答程序是否满足类型规则的判定性问题。
使用[翻译时](#实现的执行阶段)的[语义分析](#实现的执行阶段)或[运行时](#实现的执行阶段)的类型检查分别为静态类型检查和动态类型检查。
静态类型检查规则是[可诊断语义规则](#翻译时正确性规则)。
语言可能个别指定引入类型检查相关的规则,在保持逻辑相容的前提下可混合使用。
类型检查失败引起的[错误](#错误)称为*类型错误(type error)* 。
**注释**
注意静态类型检查和[静态定型](#类型系统和类型机制)以及动态类型检查和[动态定型](#类型系统和类型机制)的区别。类型检查和类型机制是不同的规则,不必然包含蕴含关系。
类型检查的一个典型的使用场景是[类型签名的兼容性](#类型标注)校验。
### 类型全集
*类型全集(type universe)* 是语言规则中允许表达的类型的总称。
**注释** 表达类型的规则构成的模型的语言是语言规则的子集。
NPL 避免限定类型全集。派生语言可指定不同的规则。
除非派生实现另行指定,程序的用户不能依赖语言规则的限定枚举类型全集中的所有类型。
**原理**
类型全集是[论域](#开放性)的实例。避免限定类型全集符合[开放世界假定](#开放性)。
#### 类型谓词
判断值是否满足类型*居留(inhabitant)* 的[谓词](#基本语义概念)是*类型谓词(type predicate)* 。
**注释**
和 \[RnRK] 的基本类型谓词不同,类型谓词定义为只接受一个参数。
### 类型序
类型之间可具有序关系。
被[定型](#类型系统和类型机制)的类型的实体可完全地满足其它类型的约束。前者具有后者的*子类型(subtype)* 。
*子类型(subtyping)* 关系是一种[预序(preorder) 关系](https://zh.wikipedia.org/zh-cn/%E9%A2%84%E5%BA%8F%E5%85%B3%E7%B3%BB),即自反的、反对称的二元关系。
相等的类型符合子类型关系,是*平凡的(trivial)* 。排除平凡的子类型关系是严格子类型关系。
严格子类型是严格预序关系,即反自反、反对称的二元关系。
子类型和严格子类型对应的逆关系是*超类型(super typing)* 和*严格超类型(strict supertyping)* 关系。
多个类型可具有公共的(严格)超类型。这些类型同为一个类型的子类型而[等价](#类型等价性)。
除非另行指定,在程序的行为不依赖其中特定的个别不相等的类型而具有差异时,具有相等超类型的等价的子类型视为相同的类型。
[复合类型](#类型)中其中一部分的类型替换为其子类型,得到的结果和原复合类型可能有如下[变化(variance)](https://zh.wikipedia.org/zh-cn/%E5%8D%8F%E5%8F%98%E4%B8%8E%E9%80%86%E5%8F%98#%E5%BD%A2%E5%BC%8F%E5%AE%9A%E4%B9%89) 的对应关系之一:
* *协变(covariant)* :类型序被保持,即结果类型是原复合类型的子类型。
* *逆变(contravariant)* :类型序的逆被保持,即结果类型是原复合类型的超类型。
* *不变(invariant)* :不保持类型序,即结果类型和原复合类型之间没有确定的子类型关系。
同时存在以下派生归类:
* *互变(bivariant)* :同时协变和逆变。
* *可变(variant)* :至少协变或逆变之一。
对接受参数类型得到结果类型的函数[类型构造器](#类型) → ,以下关系是确定的:
* 参数类型对函数类型逆变。
* 结果类型对函数类型协变。
把[LSP](#封装)要求子类型经替换前后保持性质的谓词视为类型构造器,则 LSP 要求的性质是协变的。
**注释**
关于 → 的变化关系的陈述通常直接被作为类型系统中的[定型规则](#类型系统和类型机制)表达的公理,以和 → 既有的定型规则兼容。
一些非普遍的局部类型序的构造器,如数组的下标 `[]` ,也可对参数有确定的可变关系。
在 LSP 的[原始论文](https://www.cs.cmu.edu/~wing/publications/LiskovWing94.pdf)提供了两个满足 LSP(文中称为子类型要求(subtype requirement) )的在[过程](#过程)的[签名](#类型标注)定义子类型的方法,兼容以上传统的函数类型构造器的子类型变化关系,是 → 上的上述关系的扩展:这些定义还支持表达过程的具体前置条件(precondition) 和其中引发的[异常](#异常)。
对一般的谓词,LSP 的行为多态(behavioral polymorphism) 是不可判定的。因此,一般的 LSP 无法被[类型检查](#类型检查)。在[类型系统](#类型系统和类型机制)中应用 LSP 需依赖具体能表达性质的谓词,如使用的类型构造器。
#### 类型边界元素
一个类型系统可指定唯一的[*底类型(bottom type)* (en-US)](https://en.wikipedia.org/wiki/Bottom_type) 作为其它任何不同类型的严格子类型,记作⊥。若类型全集包含空类型,则底类型是[*空类型(empty type)* (en-US)](https://en.wikipedia.org/wiki/Empty_type) 。
一个类型系统可指定唯一的[*顶类型(top type)* (en-US)](https://en.wikipedia.org/wiki/Top_type) 作为其它任何不同类型的严格超类型,记作⊤。这种类型即*通用类型(universal type)* 。
NPL 支持空类型作为底类型,但不要求在对象语言中支持其表示。
NPL 避免要求唯一的顶类型的存在以符合[开放世界假设](#开放性)。
派生语言可指定不同的规则。
**原理**
以空类型作为子类型在类型序的推理上是自然的。
就非特定的类型全集,通用类型的的构造和表示不唯一,因此不能直接断言其存在。
否则,假定存在这种类型,则断言不存在其超类型,这可能和其它语义规则冲突。
即使在名义上定义具体的超类型(如 Java 的 [`java.lang.Object`](https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html)),也面临不能向上扩展(得到比 `Object` 更基本的类型)的问题,违反[最小接口原则](#最小接口原则)和[通用性](#其它推论和比较)。
具体的顶类型在断言当前类型系统不存在公共超类型可能仍然有实用意义;此时,顶类型即[一等实体](#基本语义概念)构成的类型,而不需要定义具体[名义类型](#类型等价性)。
### 多态类型
特定的类型系统支持[类型签名](#类型标注)能对应多种不同的[兼容](#类型标注)类型。这样的类型是*多态的(polymorphic)* 。
一般地,类型上的*多态(polymorphism)* 有:
* *特设(ad-hoc)* 多态:仅对项上局部的项上的类型作用使之满足上下文兼容要求的多态:
* [函数](#函数合并)*重载(overload)* :同一个[名称](../Terminology.zh-CN.md#程序设计语言)对应的不同的函数实体,允许按[实际参数](#函数合并)的类型选择调用不同的函数。
* *强制(coercion)* :求值时使值向某个上下文要求的类型的隐式转换。
* *参数(parameteric)* 多态:接口签名指定以具体类型作为值的变量,组合为函数或者其它接口对应实体的类型。
* [子类型](#类型序)多态:接口签名编码接受子类型关系作为兼容类型。
* *行(row)* 多态:对组成具有名称和实体对构成的元素作为*成员(member)* 的实体,兼容限定部分成员的类型。
*多型(polytipic)* 的接口在同一个接口签名上以结构化类型的[隐式类型](#类型标注)构造支持不同的类型而支持多态。
**注释**
重载在一些语言中自动地对函数对应的具体[可调用实体](#过程)适用。
行多态以[结构化类型](#类型等价性)约束取代通常通过[名义类型](#类型等价性)指定的子类型关系。
### 类型种类
*种类(kind)* 是[静态类型系统](#类型系统和类型机制)的语法表示中具有特定类型模式(pattern) 的分类。
一定意义上,种类是类型系统的元语言中一种元静态类型。
一般地,实体类型的种类记作 `*` 。
除非另行指定,作为项的函数应具有函数类型,即符合类型种类为 `* → *` 的结果的类型,如为[简单类型 λ 演算](#λ-完备语义和对应语法)兼容的函数类型实例。
其中,`→` 是函数类型的类型构造器。
种类作为元语言中的[类型多态](#多态类型),实现*种类多态(kind polymorphism)* :接口签名接受类型的编码中对应位置具有不同种类的类型。
**注释**
在实现中,种类也被作为[互操作](#实现的执行阶段)的归类,如视为[函数调用](#函数调用)的[调用约定](https://www.microsoft.com/en-us/research/uploads/prod/2020/03/kacc.pdf)。
但这不足以涵盖一般的形式定义;特别地,[调用](#过程)是仅仅关于[过程](#过程)这类实体的互操作,而种类适合一般实体的静态类型。例如,在不考虑进一步地实现时,[多变(levity) 多态](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/11/levity-pldi17.pdf)的类型不需要限定过程(函数)。
类型系统中的种类也可扩展到特定的计算作用的*作用系统(effect system)* 上以描述[作用的种类](#作用使用原则),此处从略。
### 一等类型
[一等对象](#实体语义)的类型是*一等类型(first-class type)* 。
非一等类型的[居留](#类型系统和类型机制)可能不在对象语言中可表达,即对象语言中无法构造这些类型的值。
非一等类型仅用于构造其它类型(可能是一等类型)和类型检查等依赖类型的推理。
**注释**
一个典型的非一等类型的例子是 \[ISO C] 和 \[ISO C++] 等语言支持的类型 `void` 。
在语义的角度上,`void` 可视为依赖[翻译阶段](#实现的执行阶段)把[求值](#基本语义概念)时得到的对应 `void` 居留的表示替换为表示语义错误的单元类型,并在翻译结束前拒绝接受带有这种居留的程序,而这种居留在对象语言中始终不可取得。
若不限制翻译阶段,可通过在[传递](#求值策略)时始终限制[正常控制](#程序的控制执行条件)的值实现类似的效果,例如不考虑类型消除时 \[ISO C++] 中在复制或转移构造函数始终抛出异常的类类型。
## 程序的控制执行条件
程序的执行可被[控制作用](#基本语义概念)影响。蕴含这些影响的条件即*执行条件(execution condition)* 。
程序的[控制状态](#基本语义概念)决定[求值](#基本语义概念)使用的[续延](#续延)。
**注释** 这和[过程的调用](#过程)类似。
更一般地,规约规则指定语言的实现决定程序行为时使用的(对程序不保证可见的)续延,这种在实现中对应的控制状态称为控制执行条件。
和控制状态不同,控制执行条件描述语言提供的不同控制机制的分类,而不被作为语言可编程的特性提供。
除非另行指定,仅由[求值算法](#求值规约)中蕴含的规约规则决定的执行条件是*正常(normal)* 的。
[合并子调用以当前续延返回](#续延的捕获和调用)是正常执行的。
**注释** 这是正常控制执行条件的一个主要实例。
改变程序的正常的控制要求存在控制作用,此时,控制执行条件是*非正常(abnormal)* 的。
除非另行指定,隐含在求值算法中蕴含的规约规则确定的[函数应用](#函数合并)外的[续延调用](#续延的捕获和调用)是非正常的。
**注释** 这是非正常控制执行条件的一个主要实例。
具有规约语义的语言总是支持正常控制条件。NPL 中,非正常的控制条件的支持是可选的。
### 异常
由派生实现定义的非正常的控制条件是*异常(exceptional)* 条件。
*异常(excpetion)* 是通过*抛出(throw)* 实体(称为异常实体)同时表达满足异常条件的控制作用的语言构造。
语言的实现或用户通过特定操作(如求值一个表达式)指定程序满足异常条件,使程序的控制进入[异常执行状态](#正确性),允许程序具有正常条件下可分辨不同行为。
程序通过*捕获(catch)* 并*处理(handle)* 被抛出的实体,程序可满足不同的恢复正常执行的条件。
进入违反[翻译时正确性规则](#翻译时正确性规则)的异常执行状态时,由语言实现提供的异常执行机制实现行为。
**注释** 这些行为至少蕴含满足翻译时正确性规则要求的[诊断](../Terminology.zh-CN.md#程序设计语言)。
进入其它异常执行状态的异常条件包括所有[运行时](#实现的执行阶段)异常条件和直接引起程序异常的用户操作。
这些异常条件的具体行为和正常条件下的不同由派生实现指定的运行时状态或直接引起异常(改变程序的控制)或语言构造的语义决定。此时,由实现定义使用的异常执行机制。
**注释** 其它异常条件的异常执行机制可能和上述相同或不同。
派生语言实现可指定以下规则:
* 符合以上约定的判断改变(进入和退出)异常执行状态的执行机制。
* 包括抛出和捕获的语言构造和其它可选的引起改变异常条件的上下文。
若派生实现不指定以上要求的执行机制和上下文,则不支持异常。
除非派生实现另行指定,异常的控制作用总是*被同步(synchronized)* 的,即:
* 在初始化异常实体时,保证存在与异常条件关联且可确定单一的[执行线程](#并发实现)的状态作为引起控制状态改变即引发异常的来源。
* 异常条件的满足不依赖未和引发异常状态同步的程序中的其它的执行状态(包括其它未同步的线程的状态)。
* 确认满足异常条件和进入异常执行状态之间,上述执行线程内程序仅在引发异常的线程上的程序允许存在[计算作用](../Terminology.zh-CN.md#程序设计语言)(这保证不被引起可观察行为改变的其它线程的操作中断)。
除非派生实现另行指定,未捕获的异常总是确定性地(deterministically) 持续引发异常的执行线程中引起控制的转移:
* 若捕获操作有效的上下文,控制转移捕获构造处理对应异常的*异常处理器(exception handler)* 。
* 否则,若在活动函数调用中,则单向地从[当前活动的函数](#活动记录)向其[主调函数](#过程)转移控制,使后者活动。
* 否则,若没有找到剩余的活动函数调用,则程序异常终止。
除非派生实现另行指定,上述转移活动函数若成功(包括异常在活动的主调函数嵌套的特定语言构造中被捕获),先前不再活动的活动记录中的资源在控制成功转移后应立即被释放。
典型的设计中,求值规则使的正常状态的函数调用要求的活动记录分配和释放满足 FIFO(Last-In First-Out ,后入先出)的顺序,构成了*栈(stack)* ,活动记录是*栈帧(stack frame)* 。
除非派生实现另行指定,活动函数的转移释放资源,应保证按和创建被其所有的实体的顺序的相反顺序一致的形式释放。这种释放活动记录占用资源的机制称为*栈展开(stack unwinding)* 。
### 终止保证
特定的求值具有(确定性地)*终止(termination)* 保证,当且仅当预期求值总是在有限计算步骤内可描述的[计算作用](../Terminology.zh-CN.md#程序设计语言)。
具有终止保证的求值总是取得值或通过非正常控制的计算作用退出求值。
不具有终止保证的求值可能不终止,此时它具有取得值以外的计算作用;这种计算作用是[副作用](#基本语义概念)。
若一个函数的调用总是具有终止保证,则此函数是*终止函数(terminating function)* 。
若一个函数的调用总是取得值,则此函数是*全函数(total function)* 。
**注释** 全函数总是终止函数。
# NPLA
当前维护的主要派生语言为 **NPLA** ,是 NPL 的[抽象语言实现](../Terminology.zh-CN.md#规范)和[派生实现](#略称)。
NPLA 的参照实现 [NPLA1](#npla1-核心语言) 是具体语言实现,约定特定于当前参照实现的附加规则和实现。
作为原型设计,NPLA 重视可扩展性。
作为 NPL 的派生实现,NPLA [对象语言](../Terminology.zh-CN.md#程序设计语言)的设计遵循 NPL [符合性规则](#规范模型),并满足如下要求或附加限制。
**注释**
[NPLA1](#npla1-核心语言) 是 NPLA 的一个派生实现。
## NPLA 领域语义支持
* 位(bit) :表示二进制存储的最小单位,具有 0 和 1 两种状态。
* 字节(byte) :[基本字符集](#字符集和字符串)中一个字符需要的最少的存储空间,是若干位的有序集合。
* 八元组(octet) :8 个位的有序集合。
## NPLA 整体约定
### NPLA 实现环境
NPLA 使用[宿主语言](#实现的执行阶段)为 \[ISO C++11](及其之后的向前兼容的版本)的简单实现模型 [NPL-EMA](#嵌入宿主语言实现) 。
以下要求和[宿主环境](#嵌入宿主语言实现)一致:
* 一[字节](#npla-领域语义支持)占用的位(至少占用 8 个二进制位)。
* 作为事件顺序的[在先发生和在后发生](#规约顺序)和宿主语言中的定义一致。
* **注释** 为[互操作](#npla-互操作支持),一般应避免和之后的(受实现支持的)\[ISO C++] 版本冲突。
NPLA 实体的[内部表示](#表示)是宿主语言中可表达的数据结构。
NPLA 实体的[外部表示](#表示)是宿主语言中可通过输入/输出操作处理的数据。
除非另行指定,NPLA 使用宿主语言提供的异常作为[异常执行机制](#异常)。
除非另行指定,程序不使用使宿主语言*区域指定的行为(locale-specific behavior)* 改变的特性。
**原理**
默认避免改变区域指定行为简化设计约定。
**注释**
关于类似的对宿主语言程序的要求,另见 YSLib 项目文档 [`doc/LanguageConvention.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/LanguageConvention.txt) 。
### 附加功能
NPLA 支持*数值(numerical value)* ,但不要求支持具体的数值计算。
NPLA 实现为派生实现提供[数值类型](#数值类型)和相关的操作的基本支持。
除非另行指定,若派生实现支持数值计算,其实现兼容 [NPLA 数学功能](#npla-数学功能)的实现。
### NPLA 词法和语法
词法分析可接受多字节文本编码的字符串形式的源代码,但不假设其编码中除 0(空字符 NUL )以外的具体代码点被编码的数值,不[转换编码](#实现的执行阶段)。
使用可选的语法预处理和 [NPL-GA 语法](#简单文法约定)。
[字符集](#字符集和字符串)的约定同[宿主环境](#嵌入宿主语言实现)。
### NPLA 标识符
[NPL 标识符](#基本词法构造)外的以下[词素](#基本词法构造)也是 NPLA 标识符:
* 在构成 NPL 标识符的词素中插入有限个非 [NPL 分隔符](#基本词法构造)的字符且不构成 [NPLA 扩展字面量](#npla-扩展字面量)的词素。
* 全由 `+` 或 `-` 构成的词素。
### NPLA 扩展字面量
NPLA [扩展字面量](#字面量)包括:
* 以 `#` 、`+` 或 `-` 起始的但不全是 `+` 或 `-` 构成的、长度大于 1 的词素。
* 十进制数字字符起始的词素(当被支持时)。
全由十进制数字字符的词素表示十进制[数值](#附加功能)。派生实现可定义其它作为数值的词素。这些词素作为字面量时,是*数值字面量(numerical literal)* 。
### NPLA 名称和字面量求值
名称仅被实现为和[字符串](#字符集和字符串)的[值](#基本语义概念)的一个真子集一一对应的表示(参见[类型映射](#类型映射))。
除非派生实现另行指定,只有[代码字面量](#字面量)不是[自求值表达式](#范式),其余字面量都求值为[右值](#值类别)。
代码字面量求值时解释为名称。
数据字面量是自求值的字符串的[外部表示](#表示)。
数值字面量是自求值的数值的外部表示。
存在不保证先求值的[子表达式](#表达式)的[语法形式](#语法形式)是*特殊形式(special form)* 。
特定的名称是*保留名称(reserved name)* 。
除非另行指定,在[源代码](../Terminology.zh-CN.md#程序设计语言)中使用保留名称作为实体的名称的程序[行为未定义](#npla-未定义行为)。
### NPLA 求值的表示
[规范形式](#范式)是特定[类型](#类型)的 \[ISO C++] 对象。
[名称解析](#名称解析)失败可被忽略而不[终止](#范式)实现演绎;保证名称表达式求值的[强规范化](#范式)。
不要求提供[命名空间](#命名空间)实现的可变实体。
不保证求值都是[纯求值](#求值性质);非[特殊形式](#npla-名称和字面量求值)使用[热情求值](#λ-求值策略);其它情形使用热情求值或[惰性求值](#λ-求值策略)的方式由具体特殊形式约定。
对象语言的[函数](#函数)默认为[过程](#过程),过程默认实现为[子例程](#过程调用的计算顺序)。过程指定的计算结果和函数表达式[最终求值结果](#范式)的[关联](#过程)是[过程调用](#函数调用)的[结果](#基本语义概念)的恒等映射。
**注释** 即过程调用的结果总是同函数值。
除非另行指定,实现[函数](#函数)的宿主数据结构[生存期要求](#npla-基础存储模型和对象模型)默认同[宿主语言](#实现的执行阶段)。
除非另行指定,[按值传递](#求值策略)支持[复制初始化](#复制初始化和直接初始化)对象的[一等作用](#一等作用)。
**原理**
NPLA 函数不支持类似 \[ISO C++] 的类型退化(decay) 。作为动态类型语言,需要被转换的值在[操作](#规范化中间表示)内部实现,不需要在[返回值](#函数调用)上另行附加转换。
按值传递的复制初始化和宿主语言的对应语义类似。
### NPLA 类型系统
NPLA 使用[隐式类型](#类型标注)而非[显式类型](#类型标注)。
NPLA 使用[潜在类型](#类型标注):[值](#基本语义概念)具有类型;不指定[动态类型](#类型)以外的[类型](#类型)。
[显式类型(如清单类型)](#类型标注)的机制可由派生实现指定可选地引入。[用户程序](#程序实现)也可能添加类型标注和不同的[类型机制](#类型系统和类型机制)的支持。
除非派生实现另行指定,引入的[静态类型](#类型系统和类型机制)应和[动态类型](#类型系统和类型机制)一一对应。
NPLA 使用和[宿主语言](#实现的执行阶段)相容的[动态类型检查](#类型检查)。除非派生实现另行指定或[类型映射](#类型映射)的需要,使用的类型检查规则和宿主语言一致。
宿主语言对象的值描述状态,且宿主语言要求的对 `volatile` 左值的操作属于[可观察行为](#状态和行为)。
## NPLA 互操作支持
NPLA 的[宿主语言](#实现的执行阶段)应能提供 NPLA 及派生实现的[本机实现](#实现的执行阶段)。
NPLA 的派生实现提供特定的和宿主语言的[互操作](../Terminology.zh-CN.md#规范)支持,可其中和 NPLA 提供的关于互操作的具体行为不同的部分应由实现定义。
**注释** 对派生实现,NPLA 约定的具体默认互操作特性是可选的。但是,一般的约定如[开放类型系统](#类型映射)仍被要求。
NPLA 和派生实现可约定互操作的具体实现的要求,以确保实现的状态可预测。
本机实现可以具有 C++ 的实现兼容的二进制接口的函数提供,这些函数称为*本机函数(native function)* 。
本机实现可直接支持本机函数在实现中被调用。若被支持,具体接口由派生实现指定。
本机函数作为函数的实现,其调用的求值可具有和非本机的函数一致的[作用](../Terminology.zh-CN.md#程序设计语言),但不需要具有可被对象语言表达的[函数体](#函数)。
为确保函数求值的作用可能保持一致,本机函数应符合和本机函数调用时使用的规约一致的方式使用,即在宿主语言的意义上至少符合以下规约[调用约定](#函数调用):
* 被调用时的子项被作为以 [WHNF](#规范化中间表示) 形式表示的被调用的表达式使用。
* 调用后具有项被重写为必要的值以表示[函数调用](#函数调用)的[返回值](#函数调用)。
本机函数的返回值应能表达任意的非本机函数调用的返回值,即通过求值函数调用中函数体的非本机函数的[求值结果](#基本语义概念)。
**原理**
[实体的内部表示和外部表示满足实现环境的要求](#npla-实现环境)允许在宿主语言程序中直接实现关于[表示](#表示)的操作,简化了互操作机制的设计和实现。
**注释**
宿主语言自身的调用约定(通常和实现的 [ISA](#补充领域定义) 相关)作为 C++ 实现自身的 [ABI](../Terminology.zh-CN.md#规范) ,在此是中立的,没有提供特设的支持的要求。
另见 [NPLA 基础存储和对象模型](#npla-基础存储模型和对象模型)。
### 类型映射
*类型映射(type mapping)* 指定对象语言和宿主语言之间的[实体类型](#实体类型)之间的关系,是前者中的[类型](#类型)到后者中的类型的映射。
作为类型映射目标的宿主语言类型或其[子类型](#类型序)称为*宿主类型(hosted type)* 。
作为宿主语言类型的宿主类型是典型的。其它宿主类型是非典型的。
具有特定[动态类型](#类型)的对象语言的[值](#基本语义概念)在宿主语言具有宿主类型,以宿主语言的值表示,称为*宿主值(hosted value)* 。
在[互操作](#npla-互操作支持)的意义上,宿主值在作为对象语言的值的[表示](#表示)的[项](#基本语义概念)中以*宿主对象(hosted object)* 的形式被保存并可在宿主语言中访问。
对象语言的值被对象语言的实体类型表示蕴含它被映射的宿主类型表示,反之亦然。
类型映射可以是非空的多对一、一对多或一一映射。
若类型映射是一一映射,其类型等价性同宿主语言的语义规则;否则,由类型的语义规则约定。
因需提供与作为宿主语言的 \[ISO C++] 的互操作支持,所以明确约定实现中部分实体类型对应的 C++ 类型:
* 用于条件判断的单一值的宿主类型是 `bool` 。
* [字符串](#字符集和字符串)的宿主类型都是 `string` 类型。
* 和字符串的子集一一对应的[词素](#基本词法构造)的宿主类型是能映射到 `string` 的另一种类型。
**注释** `string` 是占位符,不要求是和 [\[ISO C++\] 的 `std::basic_string`](https://eel.is/c++draft/string.classes#basic.string) 相关的类型。但一般地,`string` 类型应具有和 `std::string` 相近的操作以便实现对象语言语义及支持互操作。
推论:字符串和词素可直接比较相等性或排序。
[NPLA 数值](#附加功能)在对象语言中具有[数值类型](#数值类型),具体类型映射未指定,但在 NPLA 数学功能提供可选实现。派生实现可显式扩充或替换定义其它数值类型的类型映射。
其它宿主类型[由实现定义](../Terminology.zh-CN.md#规范)。具体宿主类型参见以下各节和对象语言类型对应的描述。
宿主类型在对应的 C++ [API](../Terminology.zh-CN.md#程序设计语言) 中可能以类型别名的形式引入。
**原理**
[类型系统](#类型系统和类型机制)是[开放](#类型全集)的,可能提供不被对象语言支持的宿主语言类型和值。
但符合已指定的类型的实体需能被视为同种类型的实体使用,即子类型。
**注释**
非典型的宿主类型可以是特定的宿主类型的值的子集,即便这样的类型不被宿主语言的类型系统直接表示。
不被对象语言支持的值的一个例子是实现使用的*中间值(thunked value)* 。
关于中间值、`string` 类型的具体要求、NPLA 数学功能的规格说明和由实现定义的命名空间,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
## NPLA 未定义行为
一般地,NPLA 规则不排除[未定义行为](../Terminology.zh-CN.md#程序设计语言)。其中,[宿主语言](#实现的执行阶段)的未定义行为是非特定体系结构或其它 \[ISO C++] 意义上不可预测或不可移植的行为。
除非派生实现另行指定,NPLA 约定仅有具有以下情形的程序引入未定义行为:
* [互操作](#npla-互操作支持)中引起宿主语言的未定义行为或不满足[约定的要求](#npla-互操作支持)而可能引入派生实现定义的未定义行为。
* **注释** 例如,[没有被支持的并发访问](#npla-并发访问)引起宿主语言的未定义行为。
* [本机实现](#npla-互操作支持)无法提供资源而引起宿主语言的未定义行为(如宿主语言的实现无法提供宿主语言函数调用的自动对象隐式使用的资源)。
* 违反[资源所有权语义](#所有权抽象)约束的操作,包括但不限于:
* 违反[内存安全](#内存安全)的操作。
* **注释** 例如,违反按[项对象和关联对象所有权](#项对象和关联对象所有权)的推论不能确保满足[生存期要求](#npla-基础存储模型和对象模型)的操作。
* 除非另行指定,构造任意的循环引用。
* 使用特定的词法构造。
除非派生实现另行指定,NPLA 约定:
* 若程序的执行蕴含宿主语言中不保证排除未定义行为的操作,执行可包含宿主语言的未定义行为。
* 否则,非互操作引入的[管理规约](#管理规约)可能存在未定义行为,当且仅当它是[求值规约](#求值规约)的一部分且求值规约可能存在未定义行为。
**原理**
满足[错误条件](#错误)的程序可能[引起错误](#错误),也可引起未定义行为而不要求引起错误。这允许减少实现的复杂性。
对宿主语言的未定义行为的单独处理允许描述互操作。
程序的执行允许宿主语言的未定义行为,同时允许形式上不可靠,但仍可通过宿主的外部环境提供附加保证的实现,而保留可实现性:
* 典型地,宿主语言不保证调用的活动记录总是可用。
* 例如,\[ISO C++] 指定程序在自动对象无法分配时具有未定义行为。
* 这种情形形式上无法排除,但不影响实用(否则,任意 \[ISO C++] 程序都是不可移植的)。
* 实现仍应保守使用资源,以尽可能地避免引起宿主语言的未定义行为。
* 通过宿主的外部提供附加保证的实现类似保证为[完整性](#完整性)的前提下通过加入附加的限制来使设计符合要求。
对管理规约的约定同时蕴含对 NPLA 实现的要求。这保证未定义行为不会被任意地在对象语言以外被引入。
**注释**
为简化互操作实现,部分 NPLA 未定义行为可能在实现中被检查以预防(尽可能避免)宿主语言的未定义行为,但这种检查不保证完全覆盖所有引起未定义行为的条件,不应预期其行为可移植。
关于构造循环引用可能引起的问题,另见[内存泄漏](#资源泄漏)。
### 常规宿主资源分配要求
一般地,本机实现要求资源分配失败时,引起(可能派生)`std::bad_alloc` 或另行指定的宿主异常而非宿主语言的未定义行为;但因为宿主语言缺乏保证,可能并非所有宿主语言实现都能保证实现这项特性。
实际的实现中非极端条件下(如宿主调用栈接近不可用)通常可支持实现这些行为。
宿主语言实现支持时,具有可预期的失败(而 NPLA 或宿主语言的非未定义行为)的 NPLA 实现的要求称为常规宿主资源分配要求。
### 嵌套调用安全
宿主语言的 [API](../Terminology.zh-CN.md#程序设计语言) 提供*嵌套调用安全(nested call safety)* ,当且仅当:
若调用没有宿主语言无法分配资源的未定义行为,则同时避免因宿主语言的嵌套调用[深度](#过程)过大时引起的这样的未定义行为。
嵌套调用安全应包括支持可能通过对象语言构造的输入使对应宿主语言的操作中的嵌套调用不保证的情形。
对象语言的实现可假定限制避免无限创建[活动记录](#活动记录)即满足嵌套调用安全的要求。
**原理**
嵌套调用安全允许不限制嵌套深度的可靠的调用,如递归调用。
宿主语言实现在宿主语言的[尾上下文](#上下文相关求值)可能支持[宿主 TCO](#tco-实现策略概述) 而使递归调用满足嵌套调用安全,但这并不是语言提供的保证,不应在可移植的实现中依赖。
\[ISO C++] 并没有明确指定关于深度的限制,嵌套调用可能因资源耗尽而引起未定义行为。
严格来说,这种未指定深度是可移植性上的缺陷,因为任意小的深度的调用(甚至深度为 1 的非嵌套调用)都可引起未定义行为而不需要遵循任何 \[ISO C++] 的要求,却仍然满足实现的[符合性](../Terminology.zh-CN.md#非自指)。
\[ISO C] 也有相同的问题。
实际实现中,具体深度限制依赖实现。在宿主语言缺乏保证的状况下,添加附加假定对可实现性是必要的。
**注释**
对应宿主语言的操作中的嵌套调用不保证的情形的主要例子是保证[宿主语言中立](#宿主语言中立)。
非嵌套调用安全的情形在过程嵌套调用深度过大时,可因为宿主语言的存储资源消耗导致的宿主语言实现的未定义行为,典型地包括实现中的*栈溢出(stack overflow)* 。
不限深度的重入不一定引起无限的活动记录的创建:[尾调用](#尾调用和-ptc)应能保证嵌套调用安全。
### NPLA 并发访问
当前所有 NPLA 实现中都没有显式的[并发](../Terminology.zh-CN.md#计算机科学)访问控制,但可通过互操作引入。
**注释**
一般地,为避免并发访问引起的宿主语言的未定义行为,需要通过本机实现在外部使用不同的资源实例或附加适当的同步。
另见[并发访问安全](#并发访问安全)。
## NPLA 一等对象类型
除[类型映射](#类型映射),NPLA 约定能作为一等对象的类型支持的抽象的类型,作为实现的最小要求的一部分。
以下章节扩充 NPLA 的其它类型,这些类型中的一部分可能作为一等对象。
基于[开放类型系统](#类型映射),派生实现可定义其它类型,不论是否被[互操作](#npla-互操作支持)支持。
**原理**
这些类型在[求值算法](#求值规约)等规则的描述中适用。
### 有序对
两个不同对象可作为*元素(element)* 构成*有序对(ordered pair, pair)* 。
有序对的元素是[子对象](#子对象)。
当且仅当若有序对的两个元素不同,交换元素得到的有序对和原有序对不同。
**注释**
一些编程语言中,构造有序对的操作称为 `cons` ,有序对又称为 cons 对。
### 广义列表
*列表(list)* 一种[类型](#类型),它的对象可能具有[子对象](#子对象)。
*空列表(empty list)* 是不含有子对象的列表。其它列表是*非空(nonempty)* 列表。
每个非空列表是一个[有序对](#有序对)对象,满足:
* 有序对对象的第一个元素是列表的元素。
* 若有序对对象的第二个元素是有序对,则这个有序对对象的第一个元素是列表的元素;否则,最后一个不是有序对对象的子对象是列表的元素。
**注释** 推论:同一个列表的元素不是另一个元素的子对象;不同元素之间不具有所有权,生存期不相交。
从非空列表对象中取得元素*分解(decompose)* 列表对象。若经有限次分解,不再可取得列表对象的元素,则列表对象被完全分解。
完全分解的列表的最后一个元素之外的其它元素是列表的*前缀(prefix)* 元素。
对象具有前缀元素,当且仅当对象是列表且具有前缀元素。
*真列表(proper list)* 是空列表,或能经完全分解得到最后元素是空列表的列表。其它列表是*非真列表(improper list)* 。
**注释** 推论:非真列表是非空列表。
*广义列表(generalized list)* 是真列表或非真列表。
广义列表的元素是[一等对象](#基本语义概念)。广义列表对元素具有所有权。
广义列表是完全分解的元素的*序列(sequence)* 。
作为广义列表的非真列表是*无环的(acyclic)* ,不包含*环(cycle)* 。
**注释** 同一般的 NPL 约定,NPLA 对象不支持[自引用和循环数据结构](#自引用数据结构和循环引用)。
除非另行指定,以下列表指真列表。
*子有序对(subpair)* 是一个有序对完全分解的序列中的元素的真子集构成的子对象。
*子列表(sublist)* 是一个列表中的元素的真子集构成的列表子对象。
**注释**
无环非真列表和真列表类似,可通过 `cons` 逐次构造。
非列表的有序对的元素可能具有自引用,而不是广义列表的元素,因此不是广义列表。NPLA 的一等对象不支持这种情形。
### 符号
[*符号(symbol)*](../Terminology.zh-CN.md#程序设计语言) 是未被求值的非[字面量](#字面量)[记号](#基本词法构造)的类型。
符号值可构成[名称表达式](#λ-完备语义和对应语法)。
## 存储和对象模型
NPLA 使用统一的模型对存储和[对象](#基本语义概念)进行抽象,并提供关于存储、对象和作为对象的[表示](#表示)的[项](#基本语义概念)以及[子项](#基本语义概念)的若干保证。
对象语言的存储被视为资源进行管理,称为*存储资源(memory resource)* 。
**原理**
语言中[默认不引入非一等对象](#一等实体和一等对象)。因此,存储和对象模型作用到所有实体,有助于保持[简单性](#简单性)。
**注释**
一等对象的使用可能受到其它规则的限制,不总是能同时通过对象语言的构造创建和访问。
NPL [允许派生实现引入实体的规则](#一等实体和一等对象)不受限制。
### NPLA 基础存储模型和对象模型
因需提供[宿主语言](#实现的执行阶段)[互操作支持](#npla-互操作支持),除不支持*静态(static)* 存储和没有提供支持的存储操作外,NPLA 的基础存储模型和对象模型和 \[ISO C++11] 相同。
当前不支持的存储操作包括*分配函数(allocation function)* 取得的存储和*线程局部(thread-local)* 存储。
NPLA 还允许类似对象具有未指定的存储或不需要存储的实体,以使[一等实体](#实体语义)可涵盖宿主语言在功能上等价的非对象类型(如 C++ 的引用)。这些实体若被支持,其存储实现和互操作接口由派生实现定义。
NPLA 中不是[一等对象](#基本语义概念)的[一等实体](#一等实体和一等对象)仅由派生实现定义。
保证存储性质的差异不被依赖时,不区分一等实体和一等对象的实现方式。
在此情况下对象都是*固定(pinned)* 的,即对象在*存储期(storage duration)* 内具有宿主语言意义上的确定不变的地址。派生实现可约定扩展作为例外。
推论:若一等实体不是一等对象,存储可能和一等对象的存储方式不同。派生实现可在必要时约定与其它一等实体存储的差异。
对象的[生存期](#基本语义概念)是存储期的子集。创建对象基于已确保可访问的存储;销毁对象结束后释放存储。
NPLA [支持特定的非一等对象](#存储和对象模型)作为[引用值](#引用值)的[被引用对象](#一等引用)。
**注释** 和宿主语言类似。
作为一等对象相同方式传递的一等实体都视为一等对象。仅当不依赖一等对象的性质时,实现以[非一等对象](#一等实体和一等对象)的方式实现一等实体的操作。
**原理**
[实体的内部表示满足实现环境的要求](#npla-实现环境)决定和 NPLA 和宿主语言之间共享一些基本的假定。
### 间接值
特定的[值](#基本语义概念)是*间接值(indirect value)* 。
间接值可以*关联(associated)* 一个对象。通过间接值可以间接[访问](#基本语义概念)这个对象。
间接值可能是[一等对象](#基本语义概念)或[非一等对象](#一等实体和一等对象)。
非一等对象的间接值由实现定义,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
派生实现可以定义其它间接值,称为 NPLA 扩展间接值。
一个间接值*有效(valid)* ,当且仅当存在关联的对象且访问对象不引起[未定义行为](../Terminology.zh-CN.md#程序设计语言)。
其它间接值是*无效(invalid)* 的。
除非另行指定,通过无效的间接值试图间接访问关联的对象不满足[内存安全](#内存安全)而引起未定义行为。
有效的引用值可能被*无效化(invalidate)* 而不再有效。
派生实现可指定能使间接值无效化的操作。
因关联的对象[存储期](#npla-基础存储模型和对象模型)结束而被无效化的间接值是*悬空(dangling)* 的。
**原理**
间接值可用于代替非间接值,避免求值时改变[环境](#环境对象)所有的非[临时对象](#临时对象)的[所有权](#求值和对象所有权)。
间接值可实现和 \[ISO C++] 引用类型的表达式类似的行为。
间接访问默认没有对象的[生存期](#基本语义概念)检查,因此不是安全的。这可能被具体的间接值的规则改变。
限制具体的操作能避免或减少在可能访问间接值的操作随意引入具有潜在未定义行为风险。
**注释**
作为一等对象的间接值可能允许复制或转移关联的对象以恢复对应的非间接值作为一等对象直接访问。
在[使用约定](#间接值使用约定)后,本节以下约定要求被 NPLA 实现支持作为一等对象的间接值。非一等对象的间接值由实现定义。派生实现可以定义其它的 NPLA 扩展间接值。
#### 间接值使用约定
间接值生存期规则:被规约对象中间接值的生存期被引用的环境中的对象的生存期的子集。
不满足间接值生存期规则的情形,除非提供派生实现定义的其它保证,不保证[内存安全](#内存安全)。
以含间接值的项替代不含间接值的项,称为*引入(introduce)* 间接值。
包含间接值的项可被不含引用值的项替代,称为*消除(eliminate)* 间接值。
在特定的适当情形下实现应复制或转移间接值关联的对象以保证[满足生存期要求](#项对象和关联对象所有权),包括:
* [局部间接值安全保证](#局部间接值安全保证)描述的操作,包括[返回值转换](#返回值转换)。
* 为[互操作](#npla-互操作支持)目的,实现定义的其它情形。
* 派生实现可能定义的其它情形。
除非另行指定[引起错误](#错误),若不能满足上述适当情形条件,则[行为未定义](#npla-未定义行为)。
派生实现可基于本节约定其它规则。
**原理**
为保证间接访问关联对象的内存安全,约定间接值生存期规则。
参见局部间接值安全保证和返回值转换。
**注释**
如需直接[替换项](#项的子对象)表示的值,需消除间接值。否则,没有必要提前对项进行操作以提前移除间接值。
关于实现定义和派生实现定义的其它情形,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
另见被求值的[被规约项中的对象的所有权](#项对象和关联对象所有权)。
#### 环境间接值
[环境引用](#环境引用)间接访问[环境对象](#环境对象)。
#### 引用间接值
*项引用(term reference)* 作为间接值引用一个项,访问这个以这个项作为表示的[被引用对象](#一等引用)作为关联对象。
项引用具有[标签](#对象属性)。
### 求值和对象所有权
被求值的表达式的[内部表示](#表示)中的对象具有 NPLA 对象的[所有权](#所有权抽象)。
这些内部表示包括[环境对象](#环境对象)或[被求值的表达式中的项](#npla-表达式语义)的情形。
对象是表示它的[被规约项](#求值规约)的*项对象(term object)* 。
[NPLA 临时对象](#临时对象)的存储未指定,但[部分临时对象被项所有](#项对象和关联对象所有权)。
求值结束而不被使用的项的资源在求值终止时被释放,包括被项独占所有权的这些临时对象。
求值终止包括可被实现确定的[异常](#异常)退出。
对名义上被项所有的临时对象,必要时实现可分配内部存储转移项(包括在环境中分配),以满足附加要求(如[生存期附加约定](#生存期附加约定))。
对象的所有权随可随对象被转移,参见[对象的复制和转移](#对象的复制和转移)。
[求值结果](#基本语义概念)可以是:
* 作为[值计算](#基本语义概念)的结果的[一等对象](#基本语义概念),称为*结果对象(result object)* 。
* 传递异常的状态的实体。
* 派生实现可定义的其它实体。
**注释** 按[求值规约](#求值规约),其它的求值结果的存在未指定,若存在则可能需要其它处理,可能依赖和处理一等对象的值不同的语义规则。
[函数调用](#函数调用)时以[活动记录](#活动记录)保持[被引用对象](#一等引用)的所有权。活动记录及其帧的具体结构、维护方式和生存期由派生实现定义。
除非另行指定,NPLA 只有一种[作用域](#函数和函数应用的求值环境),这种作用域中的名称由[环境](#npla-环境)提供。
除非另行指定,NPLA 的活动记录不需要和[宿主语言](#实现的执行阶段)的结构保证直接对应关系。
**原理**
因为宿主语言函数调用实现(典型地,*调用栈(call stack)* 及其中的栈帧)不提供可移植的[互操作](#npla-互操作支持),不要求实现提供活动记录之间的映射关系。
**注释**
临时对象的存储未指定、异常退出和所有权转移类似宿主语言。
结果对象和 \[ISO C++17](由提案 \[WG21 P0135R1] 引入)中的概念对应。
另见[环境对象](#环境对象)和[环境引用](#环境引用)对其中的对象的所有权。
#### 项对象和关联对象所有权
仅在[泛左值](#值类别)中允许引入可能访问关联对象的间接值。
推论:泛左值的项对象和它作为间接值可关联的对象(若存在)不是临时对象,被环境所有。
通常纯右值作为其它项的子项而被独占所有权,求值时可能通过[临时对象实质化转换](#值类别转换)标识创建的[临时对象](#临时对象)。
[表示临时对象的项](#临时对象的表示)被纯右值所有,也间接被其它项所有。
特定的纯右值可能被[环境](#npla-环境)所有,但应只通过复制等方式访问其值而不依赖所有权关系。
关于实现中项的[宿主类型](#类型映射)和构成以及纯右值被环境所有的例子,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**原理**
基于[间接值](#间接值)的性质,为保证[内存安全](#内存安全),避免非预期地超出存储期的间接值访问,限制引入间接值的表达式的[值类别](#值类别)。
因[临时对象可能具有和一等对象不同的表示](#临时对象的表示),在此特设规则约定。
### 并发访问安全
蕴含按[抽象机语义](#实现行为)不等价副作用的并发的访问是*冲突的(conflict)* 。
不共享相同的[控制状态](#基本语义概念)的[无序](#规约顺序)的[规约事件](#规约顺序)是*潜在并发的(potentially concurrent)* 。
若程序包含[蕴含](#作用使用原则)冲突的作用的潜在并发的求值,且这些求值之间没有附加的*数据竞争避免(data race avoidence)* 保证,程序的执行包含*数据竞争(data race)* ,不满足并发访问的内存安全。其中,以下机制数据竞争避免保证:
* 所有潜在并发的求值都是宿主实现提供的*原子操作(atomic operation)* 时,避免数据竞争。
* 派生实现另行指定的数据竞争避免机制。
并发访问相关的概念和 \[ISO C++11] 相容。
### 内存安全
(非并发)*内存安全(memory safety)* 是存储资源避免特定类型不可预测错误使用的性质。
基本的内存安全保证蕴含非[并发访问](#npla-并发访问)时不引入[未定义行为](#npla-未定义行为)。这至少满足:
* 对存储的[访问](#基本语义概念)总是在提供存储的对象的存储期内,除非有其它另行指定的机制(如[宿主环境](#嵌入宿主语言实现)的[互操作](#npla-互操作支持))保证存储的访问不违反其它语义规则。
* 宿主环境中不访问未被初始化的值。
**注释** 实现仍可能因其它规则引起未定义行为;特别地,这包括[本机实现](#npla-互操作支持)无法提供资源的未定义行为。
派生实现可能扩展内存安全,提供语言规则避免非预期的内存访问错误,提供更一般的高级*安全(security)* 保证。
**注释** 例如,*保密性(secrecy)* 和*完整性(integrity)* 。
除非另行指定,派生实现不提供扩展的内存安全保证。
不满足[并发访问安全](#并发访问安全)的访问是非内存安全的。
**原理**
关于内存安全含义的讨论,另见[这里](https://arxiv.org/abs/1705.07354)。
**注释**
用户代码应注意避免违反内存安全的访问,包括非并发的,以及并发访问的内存冲突。
#### 非内存安全操作
非内存安全操作是不保证内存安全的操作,在对象语言中即可能引起违反内存安全。
这些操作违反内存安全时,引起 [NPLA 未定义行为](#npla-未定义行为),且可能未被实现检查而同时引起[宿主语言](#实现的执行阶段)的未定义行为。
对象语言中的非内存安全特性可能直接调用这些操作。NPLA 外依赖此类操作的其它操作也具有类似的性质。
**注释**
派生实现或[用户程序](#程序实现)可能使用补充检查等方式避免未定义行为。
#### NPLA 对象语言内存安全保证
NPLA 中,确定地引入具有[非内存安全操作](#非内存安全操作)的对象的操作应仅只包括引入特定的[间接值](#间接值)或其它派生实现指定类型的值的操作:
* 调用引入不保证内存安全的间接值的 NPLA [API](../Terminology.zh-CN.md#程序设计语言) 。
* 调用 NPLA 中其它取[对象](#基本语义概念)的[内部表示](#表示)的值的间接值使之被[修改](#基本语义概念)的 API 。
排除非内存安全操作以及非内存安全的本机实现,NPLA 实现的对象语言提供基本内存安全保证。
#### NPLA 内存安全保证
满足 [NPLA 对象语言内存安全保证](#npla-对象语言内存安全保证)同时排除引起宿主语言未定义行为的非内存安全的操作,NPLA 实现提供基本内存安全保证。
**注释** 宿主语言未定义行为的非内存安全的操作如超出[生存期](#基本语义概念)的[访问](#基本语义概念)。
除非通过接口约束另行指定,使用 NPLA 实现的派生实现应提供相同的保证。
**注释** 例如,添加[断言检查](#运行时内存安全检查)可能改变[实现行为](#实现行为)。
#### 运行时内存安全检查
[运行时](#实现的执行阶段)检查可能帮助排查内存安全的[实现行为](#实现行为)。这包括蕴含运行时检查的接口约束(失败时抛出异常或断言)。
此外,实现可能提供可选的运行时检查。这些可选的检查帮助排查未定义行为,而不应被程序实现依赖。
#### 局部间接值安全保证
访问间接值涉及维护[内存安全保证](#内存安全)时,可能需要[提升项](#项的子对象)[消除间接值](#间接值使用约定),以移除允许[非内存安全访问](#非内存安全操作)的间接值。
**原理**
使用[删除策略](#资源回收策略)实现[过程调用](#函数调用)时,其中分配的*局部(local)* 资源随包含资源引用的[引用](#一等引用)返回可能[逃逸](#函数调用)。一般的间接值也有类似的逃逸问题。
若其[关联](#间接值)的对象(如[项引用](#引用间接值)关联的[被引用对象](#一等引用))在调用后不再存在,则间接值不再有效,构成[悬空间接值](#间接值)。若这些间接值被[调用者](#过程)获取(如被作为[返回值](#函数调用)传递),继续[访问](#基本语义概念)这个间接值关联的对象非内存安全。
为维护内存安全保证,这些情形应被避免,如通过:
* 通过分析调用处的代码证明确保不存在这样的内存不安全访问。
* 通过间接值的消除移除这些间接值使这种悬空间接值在调用者中自始不存在。
替代消除间接值的方式包括通过*逃逸分析(escape analysis)* 替换间接值,这也能减少间接值的访问而提供更优化的实现。例如,通过对[环境中被绑定对象的使用](#环境引用)进行逃逸分析提供优化实现。
但是,这不在 NPLA 中被要求,因为:
* 逃逸分析需要完整的所有权信息,这需要附加的开销,否则不总是可行(例如涉及跨多个过程的调用)。
* 对删除策略,逃逸分析也没有提供不可替代的优化。
### 资源泄漏
*资源泄漏(resource leak)* 是不能预期地(决定性地)访问之前被分配的资源的情形。
*内存泄漏(memory leak)* 是存储资源的泄漏。
强内存泄漏状态是指存在存储无法通过任何途径访问的状态。若存在存储不被任意对象或其它另行指定的代替对象的实体(如[宿主环境](#嵌入宿主语言实现))所有权的传递闭包包含,即所有权依赖*不可达(unreachable)* ,则存在强内存泄漏。
弱内存泄漏是除了强内存泄漏以外的内存泄漏,和具体预期相关。
**原理**
一般意义下,\[Cl98] 中定义的任一空间复杂度类都可以作为形式的预期。因为内存作为存储资源被空间复杂度类度量,满足某个空间复杂度类的无*空间泄漏(space leak)* 蕴含对应的无内存泄漏。
弱内存泄漏的预期的可实现性和实现细节相关,因此 NPLA 不指定具体预期。
#### 资源回收策略
[单一作用域](#求值和对象所有权)内的资源回收有*删除(deletion)* 和*保留(retention)* 的策略。
NPLA 不限定具体使用的回收策略,但要求应支持:
* [释放一等对象时允许具有副作用](#一等状态)。
* 确保副作用作用时机的确定性,即除[用户程序](#程序实现)指定外,不在[抽象机语义](#实现行为)中延迟副作用的起始。
为简化语义规则同时避免限制特定的可用资源(如系统中剩余的内存)的变化被派生实现抽象为副作用,除非派生实现指定,不对内存使用保留策略,不使内存超出对象[生存期](#基本语义概念)。
NPLA 要求实现完全避免除用户程序显式管理资源的资源泄漏以外的强内存泄漏。
除非另行指定,NPLA 释放资源的作用顺序未指定。NPLA 不依赖释放的[作用](#基本语义概念)的顺序。
派生实现可以要求使用不同的规则:
* 指定释放资源的顺序。
* 可选地支持非确定的释放资源的副作用。
NPLA释放可能具有的副作用顺序的存储资源和其它资源共享更普遍的所有权抽象资源的[所有权语义](#所有权抽象)上的操作:
使用删除策略时,[活动的过程调用](#函数调用)对其中分配的资源具有所有权。
注意多个对象构成的系统中,仅存在[平等的所有权](#所有权抽象)时的[循环引用](#自引用数据结构和循环引用)造成强内存泄漏:除非即从循环引用的对象中区分出具有不同类所有权的对象子集实现所有权正规化,总是存在无法被释放资源的对象。
NPLA 不要求实现 GC 等机制避免这类循环引用。
关于循环引用避免,另见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**原理**
NPLA 不要求实现 [GC](#所有权抽象)。
未指定的资源释放的作用顺序使其中可能具有的副作用影响的可观察行为成为[未指定行为](../Terminology.zh-CN.md#程序设计语言)。
除非派生实现要求使用不同的规则支持非确定的资源的副作用,NPLA 的实现不依赖不保证确定性释放资源的副作用顺序的*追踪(tracing)* GC 。这使追踪 GC 可能被可选地添加(opted-in) 到实现支持特性中。这允许自动资源管理机制中一定程度的[变化的自由](#变化的自由)。\[ISO C++11] 起直至 \[ISO C++20] ,C++ 语言规则支持类似的策略。
资源释放副作用的确定性要求和作用顺序未指定的规则不影响实现使用基于引用计数的 GC 策略。这允许实现以简单的方式以用户程序不直接可见的方式引入共享资源,在避免资源泄漏的意义上兼顾[正确性](#正确性)和[简单性](#简单性)。但为[避免单一所有者](#所有权抽象),此时在对象语言应提供特性使用户程序可以创建隔离共享者的资源实体。
基于非预期的循环引用不可避免地造成实现开销而违反[避免不必要付出的代价](#避免不必要付出的代价)(即使这种开销可能并不总是可观察)NPLA 不要求实现 GC 和对一般对象区分强弱引用等机制避免循环引用。此时,程序应自行避免所有权意义上的[循环引用](#自引用数据结构和循环引用)以避免资源泄漏。
由于 GC 通常基于具有特定操作的单一资源所有权的所有者的对象池的这一实现特例,不依赖共享所有者的 GC 的设计一般也更容易满足[统一性](#统一性)、[最小接口原则](#最小接口原则)和[关注点分离原则](#关注点分离原则)。
以上规则允许程序中:
* 不依赖释放可能具有的副作用顺序的资源。
* 使存储资源和其它资源共享基于更普遍的[所有权抽象](#所有权抽象)的资源所有权语义的操作的[作用](#一等作用),以一致的方式实现资源管理。
关于不同的资源回收策略(其中一部分可能引起存储空间资源泄漏)的讨论,详见 \[Cl98] 。
使用所有权抽象活动记录的资源能更好地满足资源管理机制和具体操作的[可复用性](#可复用性)和[作用使用原则](#作用使用原则)的要求。
#### 资源回收安全性
派生实现可补充定义规则在资源回收的作用上提供更强的安全保证。
**原理**
内存泄漏是和[内存安全](#内存安全)不同的另一类非预期的问题,表明语言设计、实现或程序存在缺陷。
即便不违反内存安全保证,涉及弱化空间复杂度类预期的内存泄漏仍可损害程序的[可用性](#可用性)而引起安全(security) 问题。
内存泄漏和违反内存安全同属违反特定的存储访问不变量的*错误条件(error condition)* ,但因为不论在语言还是程序的设计和实现中,避免的机制相当不同,在此被区分对待。
即便不扩展规则提供更强的内存安全保证,仅在资源回收的作用上避免错误条件也是有意义的。
存在其它语言使用类似的区分内存泄漏和[非内存安全](#非内存安全操作)的设计,如 \[Rust](详见[相关文档](https://doc.rust-lang.org/book/second-edition/ch15-06-reference-cycles.html))。
### 子对象
对象的[子实体](#实体数据结构)是对象时,子实体是对象的*子对象(subobject)* 。
除非另行指定,子对象及其性质同宿主语言的约定:在宿主语言的表示中表现为子对象的对象语言中的对象,也是对象语言的子对象。
对象语言的其它具有子对象的情形由派生实现定义。
对象对它的子对象具有平凡的[所有权](#所有权抽象)。
对象的子对象的[生存期](#基本语义概念)不[先序](#规约顺序)对象的生存期起始,对象的子对象的生存期结束不[后序](#规约顺序)对象的生存期结束。
对象的子对象的生存期起始后序对象的生存期起始,对象的子对象的生存期结束先序对象的生存期结束。
除非另行指定,同一个的对象不同子对象的存储期起始、存储期结束、生存期起始、生存期结束之间分别无序。
对象对其存储期和生存期的其它约束和宿主语言相同。
对象可通过[子对象引用](#子对象引用)关联和与其生存期相关或无关的其它对象。
通过子对象访问的[被引用对象](#一等引用)上的[副作用](#状态和行为)是否蕴含对象上的副作用未指定。
关于内部对象,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**原理**
子对象不一定支持[可修改](#实体的不可变性)的[一等状态](#一等状态)。修改子对象可能导致或不导致对象或先前通过相同方式取得的子对象的[改变](#实体的不可变性)。
\[ISO C++] 通过类型定义具有的隐含的对象布局共享同类对象的[内部表示](#表示)。与之不同,为简化[非一等对象](#一等实体和一等对象)表示的项上的操作,子对象之间不一定共享表示。
特别地,通过[子对象引用项](#引用值的表示)访问的对象的子对象之间不一定具有[同一性](#基本语义概念)。
关于具体表示,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**注释**
作为支持子对象作为内部对象的逻辑前提,NPLA 不支持[循环引用](#自引用数据结构和循环引用)。
\[ISO C++] 允许 `const` 成员提供不支持修改的状态。[NPLA 不要求类似的类型系统支持](#npla-类型系统),没有类似的设计。
#### 项的子对象
作为对象的子项是[项对象](#求值和对象所有权)的子对象。
因为子项可以递归地具有子项,项对象作为[数据结构](#基本语义概念)构成*树(tree)* 。项对象是树的节点,即*项节点(term node)* 。
项节点具有如下互斥的基本分类:
* *枝节点(branch node)* 或*非叶节点(non-leaf node)* :具有子节点的节点。
* *叶节点(leaf node)* :不具有子节点的节点。
除子项外,项具有*值数据成员(value data member)* 作为其子对象。
表示项对象的[被规约项](#求值规约)的值数据成员提供时间复杂度为 O(1) 的操作判断:
* 项对象是否为[有序对](#广义列表)。
* 项对象是否为[真列表](#广义列表)。
* 非有序对的项对象具有的[宿主类型](#类型映射)。
值数据成员可能具有空值。
值数据成员和子项可构成[对象的内部表示](#表示):
* *列表节点(list node)* 是值数据成员为空的节点,表示真列表。
* *空节点(empty node)* 同时是叶节点和列表节点,表示[空列表](#广义列表)。
* 实现可定义其它的节点作为其它的内部表示。
若项存在其它子对象,作为对象内部表示的具体规则由实现定义。
满足以下条件的替换变换替代项或其子对象,称为项的*提升(lifting)* :被提升的项(源)是提升后得到的项(目标)的一个直接、间接子项或项的子对象变换得到的项。
提升可能包含附加检查,检查失败时可能[引起错误](#错误)而不实际进行提升。
除非另行指定,提升项[修改](#基本语义概念)被替换的对象。
**原理**
项的子对象确定的表示可能被具体的[互操作](#npla-互操作支持)依赖。
项的提升可以视为作为语法变换的消去 [λ 抽象](#λ-完备语义和对应语法)的 [lambda 提升 (en-US)](https://en.wikipedia.org/wiki/Lambda_lifting) 的一般化,但此处和 λ 抽象没有直接关联。
项的提升的变换可以是恒等变换,即直接以子对象作为替换的来源。其它变换如创建[间接值](#间接值)和取间接值关联的对象,对应的提升[引入和消除间接值](#间接值使用约定)。
项的提升的检查可包括为满足接口行为的[语义检查](#实现的执行阶段)和实现为预防[宿主语言的未定义行为](#npla-未定义行为)的附加检查。
被提升的项往往被[转移](#项的转移),因此一般地,需要在宿主语言中[可修改](#实体的不可变性)。若被提升的项表示对象语言的值,一般也需要在对象语言中可修改。
### 对象属性
除以上性质外,对象可关联其它元数据以指定对象的[属性](#基本语义概念)。
和属性对应的可组成对象的表示的非一等实体统称为*标签(tag)* 。
对象具有的标签决定以下[正交](#正交性)的性质:
* 唯一(unique) 引用:指定[对象的值](#基本语义概念)关联到自身以外的不被其它对象[别名](#对象别名)的对象。
* 以唯一引用关联的对象进行[复制初始化](#复制初始化和直接初始化)时,不需要保留关联的对象的值。
* 不可修改(nonmodifying) :指定对象的值保持不变。
* 若操作需要修改此对象,则[引起错误](#错误)。
* 临时(temporary) 对象:指定对象的值被临时使用。
唯一引用和不可修改是[引用值](#引用值)的属性。对象语言中,引用值以外的对象是否具有这些属性未指定。为[互操作](#npla-互操作支持)目的可能具有实现定义的更强的假设。派生实现也可定义更强的假设。
临时对象属性类似唯一引用,但限定的可以是对象自身而非关联的其它对象,即引用值自身和[被引用对象](#一等引用)可以分别具有临时对象属性。但除了引用值属性外,临时对象属性仅限在[临时对象](#临时对象)上出现。
**注释**
不可修改的对象类似 \[ISO C++] 的 `const` 类型的对象。\[ISO C++] 的非类和非数组类型的对象不具有 `const` 修饰。
对象的标签不在大多数对象中可见。另见[引用值的属性](#引用值的属性)。
## NPLA 环境
[求值环境](#求值环境)维护[名称](#名称)和作用域。
*变量名(variable name)* 即[变量](#基本语义概念)的[名称](#名称)。
除非另行指定,环境维护的名称都是变量名。
NPLA 的求值环境可以是:
* *一等环境(first-class environment)* ,即作为对象语言中的[一等对象](#一等实体和一等对象)的环境。
* 作为 NPLA [非一等对象](#一等实体和一等对象)的*环境记录(environment record)* 。
环境可引用若干个关联的其它环境为*父环境(parent environment)* ,用于[重定向](#名称解析)。
除非派生实现另行指定:
* 环境可引用有限多个父环境,其数量的上限未指定。
* 父环境在创建时指定,作为[实体](#基本语义概念),之后[不可变](#实体的不可变性)。
**原理**
如[需求](#需求概述)中指出的,本设计[避免命名空间隔离](https://github.com/FrankHB/pl-docs/blob/master/en-US/calling-for-language-features.md#namespace-separation),因此只有一种被环境支持且被[求值算法](#求值规约)统一处理的名称。
若派生实现需要,可修改环境的内部表示和求值算法的[名称解析](#名称解析)步骤以对不同的名称添加支持。相反,在已有多种名称的设计中还原为一种设计是更复杂和不可行的。因此,在本设计中不预设多种名称。
### 环境对象
环境作为可保持[可变状态](../Terminology.zh-CN.md#非自指)的对象,是*环境对象(environment object)* 。
环境对象包含变量名到表示[被绑定实体](#求值环境)的映射,称为*名称绑定映射(name binding map)* ,实现[变量绑定](#基本语义概念)。
被绑定实体是对象时,称为*被绑定对象(bound object)* 。NPLA 环境对象中的被绑定实体包含一等对象,因此被绑定实体总是被绑定对象。
环境对象对其中的[名称绑定映射](#环境对象)具有独占的[所有权](#所有权抽象)。名称绑定映射对其中的对象可具有独占或共享的所有权。因此,环境对象可对包括被绑定实体的名称绑定映射中的对象具有独占或共享的所有权。
环境记录之间共享所有权,以[环境引用](#环境引用)访问。
环境对象是[名称解析](#名称解析)时查找名称的[目标](#名称解析)。
父环境可共享环境记录。通过共享环境记录实现重定向的环境表示是*链接的(linked)* 而非*平坦的(flat)* 。
**原理**
仅在可证明符合语义要求等价时,使用平坦的环境表示。
对支持一等对象语义的设计,因为明确要求区分[同一性](#基本语义概念),对象的[存储](#表示)不能被任意地复制。
一般地,仅在可证明父环境对应的环境记录在对象语言和实现内部都不被共享访问(不具有[共享引用](#共享引用)且不被[别名](#对象别名)),且不存在任意派生实现定义的对释放顺序引起的可观察行为差异时,才能唯一具有这个父环境的环境为平坦的表示而[保持语义不变](#状态和行为)。
**注释**
变量名通过以和字符串一一对应的值表示,没有直接的值的限制,可能为空串。
若环境记录直接持有[被引用对象](#一等引用),则这些对象是环境记录的[子对象](#子对象)。
### 环境引用
环境引用是对象语言中访问环境记录的[一等对象](#一等实体和一等对象)。
**注释** 环境引用不是[引用值](#引用值)。后者关联的被引用对象是一等对象。
环境引用共享环境对象的[所有权](#所有权抽象)。
根据所有权管理机制的不同,环境引用包括[环境强引用](#一般引用数据结构的一般实现)和[环境弱引用](#一般引用数据结构的一般实现)。
[环境强引用](#环境引用)可能共享环境对象的所有权,对[环境对象的名称绑定映射](#环境对象)持有的项具有间接的所有权。
作为[间接值](#间接值),环境引用可被复制或转移。
复制或转移环境引用不引起被引用的环境对象被复制。因此,[按值传递](#求值策略)环境引用不引起其中所有的对象被复制。另见[引用](#一等引用)。
**原理**
区分环境对象和环境引用在纯函数式语言不是必要的,因为不需要关心[环境中的子对象](#环境对象)的复制影响[可观察行为](#状态和行为)。
否则,为支持影响可观察行为的环境的[修改](#基本语义概念),非环境记录的环境引用是必要的。
环境引用也是一种较简单且一般普遍高效的父环境的实现表示,可直接实现[链接的](#环境对象)环境而不需要证明和实现特设的其它内部表示能和[抽象机](#实现行为)意义上链接的环境保持语义等价。
[续延捕获](#续延的捕获和调用)若复制续延,可能引起关联的环境的复制,影响可观察行为并引起不必要的实现开销。为此,区分环境引用是必要的。
以环境引用作为一等对象使访问[被引用对象](#一等引用)等环境记录的子对象时需要间接访问,在环境实际不需要被复制的大部分其它场景引起开销。这种开销是可接受的,因为:
* 考虑到一等环境的普遍性,有必要有效支持对象语言中创建环境临时对象(而不仅仅是环境对象的[引用值](#引用值))的使用使之避免复制。
* 实现可能提供附加的证明以在优化的[翻译](#实现的执行阶段)过程中替换环境引用为环境记录或其它不需要间接访问的中间表示,以消除这些开销。
不论这样的证明是否存在,环境强引用和弱引用仍在对象语言中区分,以明确接口上的[所有权语义](#所有权抽象)。
引入环境弱引用作为一般的引用机制,且仅在必要时使用环境强引用,以避免过于容易[引入循环引用引起强内存泄漏](#资源回收策略),符合[适用性](#适用性)。
### 当前环境
NPLA 对象语言中,表达式的求值隐含对应一个环境对象作为[求值算法](#求值规约)需要的[上下文](#上下文相关求值)输入,称为*当前环境(current environment)* 。
## NPLA 表达式语义
本节约定对象语言中的表达式相关的语义规则,特别是[求值规则](#求值规约)。
[列表表达式](#列表表达式)作为[一等对象](#基本语义概念)是[列表](#广义列表)。
### 值类别
[表达式](#表达式)归类为具有以下基本的*值类别(value category)* 之一:
* *泛左值(glvalue)* :求值用于决定被表示的对象的[同一性](#基本语义概念)的表达式。
* *纯右值(prvalue)* :求值不用于决定对象同一性(而仅用于[初始化](#初始化)[临时对象](#临时对象)或计算对象中[存储的值](#表示))的表达式。
一个泛左值可能被标记为*消亡值(xvalue)* ,以提供基于不同的所有权的行为。
纯右值蕴含对象在[可观察行为](#状态和行为)的意义上不被共享,类似不被别名的引用的[被引用对象](#一等引用)不被[共享](#对象别名)。
*左值(lvalue)* 是除了消亡值外的泛左值。
*右值(rvalue)* 是消亡值或纯右值。
基本的值类别、消亡值、左值和右值都是值类别。
求值涉及表达式的值类别仅在必要时约定。
表达式的值类别是上下文相关的,相同表达式构造在不同的上下文可能具有不同的值类别。
NPLA 表达式允许在源语言语法之外的形式被间接构造,这些表达式同样具有值类别。
[求值规约](#求值规约)可能重写一个表达式为具有不同值类别的为被规约项。即便不能被对象语言表达,只要不和其它语义规则冲突,它们在此被视为其它形式的表达式的表示,即[项对象](#求值和对象所有权)也对应地具有值类别。
一般地,NPLA 的表达式不限定从[源代码](#表示)的[翻译](#实现的执行阶段)确定,且一个表达式的求值结果不排除继续构成表达式而被求值,因此表达式的值也普遍具有值类别。
除非另行指定,若一个 NPLA 表达式没有指定未被求值,则其值类别是其[求值结果](#基本语义概念)的值类别。
**原理**
值类别根据是否只关心表达式关联的(对象的或非对象的)值,在需要对象时提供区分[两类一等实体](#一等实体和一等对象)的机制,同时避免在仅需要表达式关联的值时引入不必要的其它对象。
**注释**
对象语言表达式的值类别和 \[ISO C++17](由提案 \[WG21 P0135R1] 引入的特性)类似。
值类别在 \[ISO C++] 中实质上是一种[静态类型](#类型系统和类型机制)系统。在 NPLA 中以更灵活的可在[运行时](#实现的执行阶段)访问的元数据代替,仍能体现类似的上下文相关性。
除了标记消亡值,附加其它元数据也允许区分不同的所有权行为。
NPLA 值类别和 \[ISO C++] 也有显著的不同,体现在如下扩展:源语言语法外的[被规约项](#求值规约)的项对象视为 NPLA 表达式,也具有值类别。
因此,作为[求值结果](#基本语义概念)的表达式的值也普遍具有值类别。若存在[结果对象](#求值和对象所有权),可直接[通过其类型确定](#引用值的子类型)。
作为静态语言,\[ISO C++] 缺乏允许在运行时确定的[求值](#基本语义概念)特性,这些不同不在 \[ISO C++] 中可用,可以被视为保守的扩展。
#### 类型系统和值类别
NPLA 中,值类别作为[实体类型](#实体类型),被作为一种内建的[类型系统](#类型系统和类型机制)。
**注释**
这和 \[ISO C++] 不同。\[ISO C++] 的“类型”的定义排除值类别,尽管值类别具有类型论意义上所有可作为类型讨论的对象的性质。
另见[引用类型](#引用值)。
### 初始化
对象被创建后可通过*初始化(initialization)* 决定其值,并可能存在其它[作用](#基本语义概念)。被决定的值是*初始值(initial value)* 。
决定初始化这些作用的表达式是初始化的*初值符(initializer)* 。
初值符的求值可能有[副作用](#基本语义概念),其求值结果指定特定被初始化的对象的初始值。
初始化包括[被绑定对象](#环境对象)的初始化和作为函数值的[返回值](#函数调用)对象的初始化。
初始化被绑定对象可能以[修改操作](#实体的不可变性)的形式体现,此时修改绑定具有副作用。若这样的副作用存在,每个被初始化的值[后序](#规约顺序)于对应初始的计算。
**注释**
初值符的求值的副作用不属于初始化,其求值结果和对象的初始值不一定相同。
和宿主语言不同,初始化不是独立的依赖特定语法上下文的概念,但此处语义上的作用类似。
对象的初始化一般可蕴含[子对象](#子对象)的初始化。
#### 复制初始化和直接初始化
初始化包括*直接初始化(direct initialization)* 和*复制初始化(copy initialization)* 。
函数可能接受[引用值](#引用值)参数和返回值,是对函数的形式参数或函数值的复制初始化;其它初始化是直接初始化。
复制初始化形式参数和函数值时,函数参数或返回值作为初值符。
**注释**
区分两者和宿主语言类似。
#### 函数参数和函数值传递
部分函数可保证[被初始化的对象副本](#实体的副本)中的值和初值符的值及元数据一致。
这样的参数或返回值的初始化的求值称为*转发(forwarding)* 。
转发也包括只部分保留上述部分元数据的情形。
在允许保留元数据不变的上下文,转发在[本机实现](#npla-互操作支持)中可直接通过[转移项](#项的转移)实现。
转发保持引入这些初始化的表达式(通常是被求值取得函数值的函数表达式)时,其求值结果(函数值)的[值类别](#值类别)和初值符保持一致。
**注释**
这里的元数据的一个例子是[引用值的属性](#引用值的属性)。
转发类似宿主语言的*完美转发(perfect forwarding)* 。
另见[函数值传递](#求值策略)。
#### 对象的复制和转移
可使用初值符为参数进行复制或转移操作以[复制初始化](#复制初始化和直接初始化)对象,创建[对象的副本](#实体的副本)。
**注释** 这类似宿主语言中的类类型的值。其它情形另见[复制消除](#复制消除)。
对象的复制和转移不改变被转移后的[类型](#类型)。
对象的复制和转移对应蕴含其子对象被复制和转移。在[互操作](#npla-互操作支持)的意义上,若项[具有子对象的独占所有权](#求值和对象所有权),这些子对象的复制构造函数和转移构造函数被对应调用。特别地,这里的子对象包括[宿主值](#类型映射)。
可使用转移操作时,不对作为对象的[表示](#表示)的[项](#基本语义概念)进行复制,因此不要求其中的子对象可复制,而避免[引起错误](#错误)。
**注释** 这类似 \[ISO C++11] 起选择类的转移构造函数代替复制构造函数。
和 \[ISO C++11] 起不同,上述可使用转移操作的条件和语法上下文无关:引起选择转移操作的条件由对初值符的谓词而非类似宿主语言的构造函数判断(详见[默认值类别转换约定](#默认值类别转换约定))。
**注释** 同宿主语言。
除非另行指定,需要创建实体的副本时:
* 若对象满足[可转移条件](#对象的可转移条件),则转移而不是复制。
* 其它情形实体被复制。
**注释** 一个主要的实例是[按值的副本传递](#求值策略)。
#### 项的转移
一定条件下,作为对象的[表示](#表示)的[项](#基本语义概念)可被整体转移,而避免其中包含的对象的初始化在对象语言中具有可见的[作用](#基本语义概念)。
在[互操作](#npla-互操作支持)的意义上,因作为对象的表示的项的转移,项及其子对象的转移构造函数会被调用,但项的[值数据成员](#项的子对象)中的[宿主类型](#类型映射)的转移构造函数不会被调用。
**注释** 这一般要求实现使用某种[类型擦除](#类型标注)使子对象类型的转移构造函数的调用不蕴含宿主类型的转移构造函数的调用。
项的转移是[析构性转移](#实体的副本)。
一般地,当对象需要被转移且没有约定[转移后要求类型不变](#对象的复制和转移)时,项的整体转移可代替[对象的转移](#对象的复制和转移),避免初始化新的[宿主对象](#类型映射),称为宿主对象转移消除。
**注释** 若需调用[宿主类型](#类型映射)的转移构造函数,需明确避免在代替对象的转移的上下文中进行操作。派生实现可提供这些操作。
[返回值转换上下文](#返回值转换上下文)的转移蕴含宿主对象转移消除。
若被[复制消除](#复制消除)的对象来自不同的项,则复制消除蕴含宿主对象转移消除。这包括所有对象转移的返回值转换上下文的情形。
### 引用值
在对象语言中,*引用值(reference value)* 是作为[引用](#一等引用)的[值](#基本语义概念),可保存在一等对象中。这样的一等对象是*引用对象(reference object)* 。
引用值和引用对象的值具有*引用类型(reference type)* 。
在特定上下文中,引用和其它[一等对象](#一等实体和一等对象)的值的相同具有不同的语义,主要体现在引用值被按值直接初始化传递和[按引用传递](#求值策略)时。
**注释** 差异和 \[ISO C++] 中使用对象类型和引用类型作为参数类似。
NPLA 引用值总是假定和[被引用对象](#一等引用)关联。
**注释** 和宿主类型类似,引用类型没有空值。
仅当以下情形中,NPLA 引用值的被引用对象是非[一等对象](#一等引用):
* 作为[临时对象](#临时对象)的[被绑定对象](#环境对象)。
**原理**
由于[左值](#值类别)的[项对象](#求值和对象所有权)[被环境所有](#项对象和关联对象所有权),为允许[规约求值](#求值规约)其中的[被绑定对象](#环境对象),需要不被环境所有的(其它不同的)[被规约项](#求值规约)作为[表示](#基本语义概念)的[项对象](#求值和对象所有权)作为中间值。
这种中间值通过间接引用作为[一等对象](#基本语义概念)使用,也是[一种间接值](#引用间接值),即引用值。
#### 子对象引用
特定的引用值是*子对象引用(subobject reference)* ,其被引用对象是被另一个对象所有的、作为这个对象的[子对象](#子对象)的一等对象。
子对象引用对特定操作可表现和其它一等对象不同的行为。
以下引用是子对象引用:
* *子有序对引用(subpair reference)* 是[子有序对](#广义列表)作为被引用对象的引用。
* *子列表引用(sublist reference)* 是[子列表](#广义列表)作为被引用对象的引用。
语言可能引入其它的子对象引用。
#### 引用值的有效性
[作为](#引用间接值)一种[间接值](#间接值),引用值有效当且仅当访问被引用对象不引起未定义行为。
以下约定要求被 NPLA 实现支持的有效的引用值总是无条件地允许访问对象。
有效的引用值应通过特定的构造方式引入,包括:
* 在对象语言通过被引用对象初始化引用值。
* [互操作](#npla-互操作支持)引入的保证不引起未定义行为的引用值。
**注释**
一些对象语言的操作可能引起引用值无效。例如,[改变](#实体的不可变性)被引用对象可以使已被初始化的有效的引用值成为*悬空引用(dangling reference)* 。
#### 多重引用
被引用对象也可以是引用值。
被引用对象不是引用值的引用值是*完全折叠(fully collapsed)* 的。
除非另行指定,*未折叠的(uncollapsed)* 引用值指未完全折叠的引用值。
**注释**
这和宿主语言不同。
#### 引用值的属性
引用值可以具有和作为引用值[表示](#基本语义概念)的[项](#基本语义概念)保存的[属性](#对象属性)相互独立的属性,保存其作为一等对象的状态。
属性不可分割:一个引用值明确具有或者不具有一种属性。
和[对象属性](#对象属性)对应,NPLA 指定的引用属性可以是:
* 唯一引用。
* 不可修改引用。
* [临时对象](#临时对象)引用。
引用值属性指定通过引用对[被引用对象](#一等引用)的访问假定允许具有的性质,即便被引用对象自身没有具有这些属性。
特定的操作使用引用值作为操作数,根据不同的属性决定行为,包括在违反属性引入的假定时[引起错误](#错误)。
在本节要求以外,除非派生实现另行指定,违反这些假定不引起 [NPLA 未定义行为](#npla-未定义行为)。
具体的引用属性满足以下语义规则:
* 唯一引用允许通过引用值访问被引用对象时,对象可被假定不被其它引用而仅通过这个途径访问,即便实际存在其它途径的引用时可能引起不同的行为;在假定的基础上程序具有何种可能的行为是未指定的。
* 唯一引用可被假定不被共享,被引用对象不被[别名](#对象别名)。
* 通过不可修改引用的左值的对象访问不包含[修改](#基本语义概念)。否则,若没有引起错误,程序[行为未定义](#npla-未定义行为);但除非另行指定,不引起[宿主语言的未定义行为](#npla-未定义行为)。
* 具有临时对象引用属性的引用值是临时对象的引用值,其被引用对象是临时对象。
**原理** 宿主语言的互操作不被总是要求保证对象语言程序的可移植性,但不应引起实现自身的行为无法预测。
对引用值的操作*传播(propagate)* 特定的引用属性,当且仅当:
若操作数是具有特定引用属性的引用值,且结果是引用值时,结果具有和操作数相同的特定属性。
**注释**
引用值属性和对象属性相互独立,类似 \[ISO C] 和 \[ISO C++] 在指针和引用等复合类型的 `const` 等限定独立于指向的对象或被引用对象上的类型不同。通过 `const` 等属性可以在指针或引用类型上单独限制类型,而不影响对应的被间接访问的对象。
唯一引用蕴含的假定类似 \[ISO C] 约定的 `restrict` 关键字,但程序违反假定的约束时不引起未定义行为。
和 \[ISO C++] 核心语言(但不是 [\[res.on.arguments\]](https://eel.is/c++draft/res.on.arguments) 中的标准库绑定到右值引用实际参数的约定)的右值引用类似,唯一引用不总是表示被引用对象不被共享。
接受唯一引用的操作可能只假定被引用对象的[子对象](#子对象)不被共享,也可能完全不进行假定,这依赖具体操作的语义。若需要和具体操作无关的无条件非共享假定,使用[纯右值](#值类别)而非作为左值的唯一引用。
和宿主语言的 `const` 限定类型类似,不可修改引用仅针对特定左值的访问;通过共享的其它未被限定的引用仍可修改对象。
违反不可修改引用引入的假定的错误可能通过[类型检查](#类型检查)或其它方式引起。
临时对象引用类似 \[ISO C++] 的*转发引用(forwarding reference)* 中保留在表达式声明中的类型信息。
因为 NPLA 不支持声明元数据,这些信息保存在对象的[表示](#表示)中,且在初始化时被引用值保存;也因此[这些元数据可跟随一等对象传递](#绑定临时对象属性)。对临时对象,[绑定操作](#绑定操作)可确保[元数据被添加](#绑定临时对象属性)。
这也和宿主语言不同。在宿主语言中:
* 无论是标记消亡值的右值引用类型还是标记是否可转发的引用的转发引用推断的类型信息(左值引用或右值引用)都是静态的。
* 并且,转发的类型信息只在函数模板的局部有效,而不存在对应的跨过程传递机制。
#### 引用值的消除
作为[间接值](#间接值),引用值可被[消除](#间接值使用约定),即被其[(可能多重)引用](#多重引用)关联的被引用对象替代。
[未折叠的引用值](#多重引用)消除一次引用值,结果仍是引用值。
消除[完全折叠的引用值](#多重引用)的结果总是[右值](#值类别)。
推论:因为引用值不[循环引用](#自引用数据结构和循环引用)自身,除非引用值[已完全折叠](#多重引用),继续消除引用值得到的值和引用值是不同的值。
**原理**
特定的引用值消除可蕴含对不可修改的[传播](#引用值的属性)的要求。这和 \[ISO C++] 初始化引用时遵循的 `const` 安全性,属于类型安全性的一种。
但是,消除引用不一定总是预期这种性质,特别当折叠不被预期时。
例如,\[ISO C++] 内建指针的不同级 `const` 不会被隐式转换直接折叠合并。消除间接的指针值不是隐式的(而依赖内建一元 `*` 操作符),这是因为指针作为类型构造器自身的类型安全需要;是否消除 `const` 限定符仍然需要基于其它理由考虑。
而当被引用对象实现子对象时,修饰被指向的类型的 `const` 不会自动传播到子对象的类型中,此时可有 `std::experimental::propagate_const` 可选引入这种性质。
对具有非间接访问的子对象的类型,这相当于 \[ISO C++] 的 `mutable` 修饰符,可实现[内部可变性](#实体的不可变性)。而允许子对象以外直接不传播不可变性,是一种结构性的平凡的扩展:这允许把被引用对象直接视为一种子对象的实现,而非要求引入新的[名义类型](#类型等价性)。
在 NPLA 这样没有要求显式类型编码是否可变的语言中,首先要求总是具有不可修改的传播性质会显著增加规则形式上的复杂性。若具体操作需要传播不可修改性,仍可进一步约定。
**注释**
典型地,消除引用值包括:
* 当[引用值提升转换](#值类别转换)的操作数是引用值时,消除被提升的引用值。
* 当[引用折叠](#引用折叠)的操作数是引用值时,消除被折叠的引用值。
和引用折叠不同,引用值提升转换不满足对不可修改引用属性的传播性质。
#### 引用折叠
和 \[ISO C++] 类似,引用值在 NPLA 中默认不被继续引用,使用引用初始化引用会引用到[被引用对象](#一等引用)上,即*引用折叠(reference collapse)* 。
引用值被折叠后结果和原引用值不同,当且仅当原引用值是[未折叠的引用值](#多重引用)。
和 \[ISO C++] 不同,NPLA 不限制派生实现利用未折叠的引用值。
**注释** 特定的操作可能区分未折叠的引用值。
引用折叠的结果是不可修改引用,若引用值和作为引用值的被引用对象之一是不可修改引用。
引用折叠的结果满足不可修改[引用属性的传播性质](#引用值的属性)。推论:
* 引用折叠的结果是[唯一引用](#引用值的属性),当且仅当引用值和作为引用值的被引用对象都是唯一引用。
* 引用折叠的结果是[临时对象引用](#引用值的属性),当且仅当被引用对象是临时对象引用。
**原理**
内部表示可支持间接的引用,以允许在对象语言中实现[一等引用](#一等引用)。
引用折叠对不可修改的传播性质的要求和 \[ISO C++] 的引用折叠对 `const` 限定符的处理类似。
引用折叠对唯一引用的要求和 \[ISO C++] 的右值引用仅通过被折叠的引用都是右值引用类型折叠类似。注意 \[ISO C++] 右值引用推断仅用于推断转发引用(forwarding reference) 参数,而非直接声明特定的右值引用类型。
和唯一引用不同,临时对象相对唯一引用更接近 \[ISO C++] 的声明的右值引用类型信息(而非推断值类别时使用的消亡值表达式的右值引用类型),一般不预期被折叠。
**注释**
未折叠的引用值被折叠时,用于初始化的被引用对象可能仍然是未折叠的引用值。
#### 对象的可转移条件
根据项是否具有特定元数据的引用值可判断使用复制代替[对象转移的条件](#对象的复制和转移)。
对象的*可转移(movable)* 条件的判断基于首先基于[值的类型](#表达式的类型):
* 非引用值(纯右值)总是可转移的。
* 否则,对象是引用值。可转移由[引用值的属性](#引用值的属性)决定:当引用值是唯一引用且非不可修改,引用值是可转移引用,对应的被引用对象是可转移的。
#### 引用值的表示
作为引用值的[表示](#表示),*引用项(reference term)* 是包含[项引用](#引用间接值)的[项](#基本语义概念)。
引用项中的项引用对象引用一个(其它的)项,即*被引用项(referenced term)* ,用于在必要时引入可被引用的一个项而不在 TermNode 中直接储存这个项的值。
被引用项表示引用项作为引用值对应的[被引用对象](#一等引用)。
引用项在作为[项对象](#求值和对象所有权)外,[保存标签](#引用间接值)作为[引用值的属性](#引用值的属性)的[表示](#表示)。
[临时对象](#临时对象)可作为引用值的被引用对象。
与此不同,非临时对象的引用值可作为一等对象而总是需要区分作为不同对象的[同一性](#基本语义概念)。
带有[临时对象属性](#引用值的属性)的引用值可在特定的操作中被视为和临时对象引用近似的引用值。
子对象引用的表示是*子对象引用项(subojbect reference term)* ,和本节中的其它引用类型的表示兼容,但不完全相同。
关于引用项的构成,另见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**原理**
因为[临时对象不是一等对象](#临时对象),临时对象的引用值可代替关联的被引用对象使之作为一等对象被访问。
为在对象语言中区分引用值和非引用值的[一等对象](#基本语义概念)是必要的,引用项这样的特设表示是必要的。
非引用项的表示则是针对临时对象的一种优化,因为使[被引用对象](#一等引用)总是在作为引用值的表示而:
* 相比通常的间接值,避免间接[访问](#基本语义概念)关联的[被引用对象](#一等引用)的开销。
* 避免[悬空引用](#引用值的有效性)。
带有临时对象属性的引用值和临时对象的引用值不同,参见[绑定临时对象属性](#绑定临时对象属性)。
#### 引用值的子类型
根据表示和属性,引用类型具有如下[子类型](#类型序):
* *左值引用(lvalue reference)* :以引用项表示的非[唯一引用](#引用值的属性)。
* *右值引用(rvalue reference)* :以引用项表示的唯一引用。
引用值是否作为[左值](#值类别)使用取决于上下文。除非另行指定,引用值都是左值。
**注释** 在要求右值的上下文发生[左值到右值转换](#默认值类别转换约定)。
引入不同的引用子类型后,NPLA 一等对象的值的类型和[值类别](#值类别)存在以下一一对应关系:
* 若类型是左值引用,则对应的值类别是左值。
* 若类型是右值引用,则对应的值类别是消亡值。
* 否则,对应的值类别是右值。
**原理**
左值引用和左值引用与宿主语言中的对象类型的左值引用与右值引用分别类似。
**注释**
在要求右值的上下文,作为左值的引用值发生[左值到右值转换](#默认值类别转换约定)。
#### 不安全引用值
特定的引用值是*不安全引用值(unsafe reference value)* ,可能和常规的其它引用值具有不同的内部表示。
若实现支持不安全引用值,和其它引用值的行为不同由实现定义。
派生实现可能添加更多对不安全引用值的假设。
**原理**
不安全引用值可能放弃常规的引用具有元数据而能被更高效地访问。
### 值类别转换
具有特定值类别的表达式可转换为不同值类别的表达式:
* 除非另行指定,泛左值总是允许作为纯右值使用。从泛左值取对应右值的操作称为*左值到右值转换(lvalue-to-rvalue conversion)* 。
* 从纯右值[初始化](#初始化)可被对象语言作为[一等对象](#基本语义概念)使用的[临时对象](#临时对象)的引用值作为[消亡值](#值类别),称为*临时对象实质化转换(temporary materialization conversion)* 。
左值到右值转换没有[副作用](#一等状态)。临时对象实质化转换没有副作用,当且仅当其中初始化临时对象时没有副作用。
临时对象实质化转换中,纯右值被*实质化(materialized)* 。
在求值子表达式时,按表达式具有的语义,必要时(如按[相关规则](#类型系统和值类别)判断上下文的值类别)进行值类别转换。
NPLA 还提供可能使结果具有不同的值类别的*引用值提升转换(reference value lifting conversion)* 。以下规则确定引用值提升转换的结果:
* 若操作数是引用值,则结果是操作数的[被引用对象](#一等引用)。
* 否则,结果是操作数。
引用值提升转换蕴含引用[提升](#项的子对象),即使用被引用对象替换操作数。
**原理**
为支持[引用值](#引用值)作为一等对象(特别是[未折叠的引用值](#多重引用)),NPLA 提供比左值到右值转换更精细的引用值提升转换。
值类别转换在特定求值中适用,因此不影响构造性的规则。
特别地,列表左值(列表的引用值)不能代替列表,因此以空列表的引用作为最后一个元素的嵌套有序对是[非真列表](#广义列表)。这和 \[R7RS] 约定空列表总是同一对象不同。
这种设计使语言规则更容易在局部一致,同时显著减少实现(对象内部表示)的复杂性,并有助于提升实现性能的可预测性。
**注释**
不同值类别表达式的转换和宿主语言中的部分标准转换类似。
根据引用值的性质,易知左值到右值转换的规约是引用值提升转换的规约的传递闭包,即:
* 若操作数是[已完全折叠的引用值](#引用折叠),则引用值提升转换等价左值到右值转换。
* 否则,有限次的引用值提升转换等价左值到右值转换。
引用值提升转换不[传播引用值的属性](#引用值的属性),参见[引用值的消除](#引用值的消除)。
引用值提升转换不传播不可修改属性,类似 \[ISO C++] 非引用值的转换在结果中不保留源操作数中的 `const` 类型。
临时对象实质化可实现为空操作,因为项在先前(如[返回值转换](#返回值转换)蕴含的[引用值提升转换](#值类别转换)对[引用项](#引用值的表示)的[提升操作](#值类别转换)的实现中)已被创建。
互操作可能引入不以项表达的右值而需要首先创建项。
#### 默认值类别转换约定
除非另行指定:
* 作为[操作符](#规范化中间表示)的[表达式](#表达式)没有值类别转换。
* 作为[操作数](#规范化中间表示)的表达式仅在必要时进行一次左值到右值转换。
**原理**
类似宿主语言规则,并非所有上下文都需要转换。类似地,宿主语言的操作符(括[函数调用](#函数调用)的第一个子表达式)可直接使用左值而不需要转换。但和宿主语言不同,因为[多重引用](#多重引用),不确定次数的连续的转换结果不同。因此除了上下文要求,有必要约定默认仅转换一次,而非确保转换结果到右值。
必要时,具体[操作](#规范化中间表示)仍可指定不同的规则。
值类别和左值到右值转换在一些上下文的行为类似箱和自动拆箱,约定存在默认转换并不利于维护[简单性](#简单性):
* 特别地,和宿主语言不同,函数不包含充分的信息(参数类型)推断是否接受左值操作数,因此在不提供针对函数的*重载(overloading)* 一般机制的前提下,[本机实现](#npla-互操作支持)不能预知输入的操作数是否是左值,通常需分别支持左值和右值的操作数。
* 即便提供重载,仍然较单一的值类别更复杂。
但 NPLA 的设计中,值类别转换已被通过[正确](#正确性)反映需求的[存储和对象模型](#存储和对象模型)的设计隐含在项的内部性质中,因此不是可选的。
由[正确性的优先规则](#正确性),[完整性](#完整性)应先于简单性被满足。
而考虑[统一性](#统一性),对存储和对象模型的设计,用户自行的实现仍要求这些设施(尽管更困难)。
关于箱和自动装箱,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
#### 返回值转换
*返回值转换(return value conversion)* 是一次引用值提升转换和可选的一次临时对象实质化转换的顺序复合。
返回值转换用于在对象语言中确定函数调用的[返回值](#函数调用)可包含[函数体](#函数)的求值结果到返回值的转换。
引用值作为间接值,适用[局部间接值安全保证](#局部间接值安全保证)。在[返回值转换上下文](#返回值转换上下文)中确定函数返回值的[实质化转换上下文](#实质化转换上下文)的部分操作[消除引用值](#引用值的消除),即返回值转换,是这种情形的主要实例。
这可约束作为间接值的[完全折叠的引用值](#多重引用)不[逃逸](#函数调用)(因此[访问被引用对象的值可不超出指向对象的存储期](#局部间接值安全保证)),而保证只考虑项可能是引用值时的内存安全。
除非证明不需要[临时对象](#临时对象),返回值转换中初始化临时对象作为返回值的[项对象](#求值和对象所有权),否则临时对象被[复制消除](#复制消除)。是否存在复制消除是未指定行为。
不论是否存在返回值转换,返回值的项对象来自返回的[右值](#值类别)关联的[临时对象实质化转换](#值类别转换)。这可能在返回值转换蕴含的[项提升](#项的子对象)操作或之前的[求值规约](#求值规约)中蕴含。
**注释**
返回值转换不保证[未折叠的引用值](#多重引用)在消除引用值后的结果不逃逸。
为确保内存安全,程序仍需要保证被引用的对象的间接引用的对象生存期结束后,不能访问间接引用的对象。
其它间接值的内存安全需要另行保证。
是否需要返回值转换由实质化转换上下文中的被调用的函数而非上下文是否需要使用右值决定,无关被转换的表达式是否是左值,因此返回值转换不是[左值到右值转换](#值类别转换)。
当前未实现是否需要临时对象的证明。
另见[项对象和关联对象所有权](#项对象和关联对象所有权)和[局部间接值安全保证](#局部间接值安全保证)。
### 临时对象
特定的 [NPLA 非一等对象](#npla-基础存储模型和对象模型)是*临时对象(temporary object)* 。
NPLA 允许(但不要求对象语言支持)一等对象构成的表达式通过特定的求值,在中间结果中蕴含这种非一等对象。
**注释** 这样的非一等对象不在源语言中可见,一般仅用于[互操作](#npla-互操作支持)。
临时对象的[子对象](#子对象)不是临时对象。
NPLA 对象语言在特定的上下文引入其它临时对象,包括:
* [实质化转换上下文](#实质化转换上下文)。
* [返回值转换上下文](#返回值转换上下文)。
**原理**
为简化规约和互操作机制的设计,和 \[ISO C++17] 不同,引入临时对象不包括延迟初始化或异常对象的创建。
关于临时对象的子对象的规则,参见[绑定临时对象](#绑定临时对象)中的原理。
**注释**
关于临时对象的存储和所有权,参见[求值和对象所有权](#求值和对象所有权)。
关于临时对象的表示,参见[临时对象的表示](#临时对象的表示)。
关于避免特定相关对象的[初始化](#初始化)的要求,参见[复制消除](#复制消除)。
引入临时对象的一些上下文的和宿主语言类似。
#### 实质化转换上下文
可具有(但不保证具有)[临时对象实质化转换](#值类别转换)的上下文包括:
* 使用纯右值初始化被绑定为[引用值](#引用值)的变量(如函数的引用类型的[形式参数](#函数内部的变量))。
* 求值[函数调用](#函数调用)以初始化[函数值](#函数合并)。
**注释**
一般地,被绑定为引用值的变量在[活动调用](#函数调用)关联的环境分配临时对象。此时,对象被调用表达式的项独占所有权,同时被绑定的环境独占资源所有权,并实现[复制消除](#复制消除)。
在不具有转换时,优化的实现可能消除函数调用(内联(inline) 展开)而不分配关联的环境,把临时对象分配到其它环境或者语言不保证可见的存储(如 CPU 寄存器)中,并同时实现复制消除。
临时对象实质化转换引入临时对象的规则和 \[ISO C++17] 不同:
* 不论表达式是否作为子表达式使其值被使用(未使用的情形对应 \[ISO C++] 中的 discarded-value expression ),都允许存在临时对象。
* 要求复制消除而避免特定对象的[初始化](#初始化)。
#### 返回值转换上下文
[返回值转换](#返回值转换)可引入实质化的临时对象,其中[可能转移求值](#对象的可转移条件)的中间结果;否则,对象被复制。
此处被转移对象符合[求值和对象所有权](#求值和对象所有权)规则中的临时对象的定义,但除非另行指定,被转移的对象不在对象语言中可被访问。
仅在对象被复制且复制具有副作用时,返回值转换具有等价复制的副作用。
#### 复制消除
NPLA 要求特定上下文中的*复制消除(copy elision)* ,排除复制或转移操作且保证被消除操作的源和目的对象的[同一性](#一等对象的同一性)。
复制消除仅在以下转换上下文中被要求,即直接使用被转换的源表达式中的对象作为实质化的对象而不初始化新的临时对象:
* [实质化转换上下文](#实质化转换上下文)。
* 引起对象转移至[函数值](#函数合并)的[返回值转换上下文](#返回值转换上下文)。
非[本机实现](#npla-互操作支持)函数的函数体内指定的返回值不属于上述的确定返回值的上下文,但也不要求被复制消除。
实现仍可根据[当前环境](#当前环境)来判断是否在允许消除对象复制的上下文中,而进行复制消除。
复制消除不在被初始化对象以外引入新的对象语言可见的对象。
**原理**
为维护语言规则的[简单性](#简单性)和使用这些规则的程序的行为的[易预测性](#易预测性),NPLA 的复制消除限于临时对象的消除。
在完成实质化转换前的不完整的[求值规约](#规约规则和求值)中的临时对象在逻辑上不需要作为[一等对象](#基本语义概念)存在,但纯右值作为对象[表示](#表示)中的[子项](#基本语义概念),随纯右值在宿主语言中作为对象存在,以允许互操作。
复制消除的目的 \[ISO C++17] 类似。同时,提供语言支持也允许更简单地实现 C++ 互操作性。
和 \[ISO C++17] 不同的一些要求可简化语言规则和实现,例如:
* 不区分求值结果是否被作为[返回值](#函数调用)或求值是否为常量表达式。
* 非[本机实现](#npla-互操作支持)函数的规则不要求 `return` 语句中的特定的表达式,而不需要依赖特定上下文的语法性质。
* 同时,NPLA 不限制对象的类型(\[ISO C++17] 则要求特定的 C++ 类类型)。
**注释**
在实现中,被转换的源表达式中的对象是[待求值项](#求值规约)的[项对象](#求值和对象所有权)。
当前未实现按当前环境判断是否在允许消除对象复制的上下文中进行复制消除。
#### 生存期扩展
在使用[纯右值](#值类别)初始化[引用值](#引用值)时,*扩展(extend)* 源表达式的[项对象](#求值和对象所有权)的[生存期](#生存期附加约定)使之比其它规则决定的生存期延长。
这和初始化非引用值类似,但实现需区分是否初始化的是延长生存期的临时对象,以确保之后能区分引用值初始化时是否[按引用传递](#求值策略)。
#### 绑定临时对象属性
若[实质化转换上下文](#实质化转换上下文)支持[绑定临时对象](#绑定临时对象),按引用绑定(即绑定初始化使用[按引用传递](#求值策略))的[被绑定对象](#环境对象)是[临时对象](#临时对象)。
引入引用值的形式参数需要满足的要求由引入绑定的操作或派生实现指定。
**原理**
绑定临时对象时指定临时对象属性允许区分[通过引用绑定延长生存期的临时对象](#生存期扩展)和非引用绑定的对象。
一般地,表达式中的[纯右值](#值类别)(非引用值)被绑定为临时对象,即被绑定的对象在初始化后具有[临时对象属性](#对象属性)。
这对应宿主语言中的转发引用参数(如 `std::forward` )中的情形:
* 若模板参数 `P` 对应转发引用函数参数 `P&&` ,其中 `P` 是对象或对象的右值引用类型,保留从实际参数*推导(deduce)* 得到的但不是实际参数类型的信息。
* 没有绑定临时对象属性的对象则同一般的非引用类型的对象类型参数(非转发引用)。
`P` 在宿主语言中通过值类别推断,但不表示值类别。
类似宿主语言,这种操作数表达式的值类别以外的形式是一种[类型推断](#类型标注)。因为推断规则和宿主语言的*类型推导(type deducing)* 相似,这种上下文可支持类似宿主语言的参数转发。但和宿主语言的 `std::forward` 不同,此处推断的右值除了是[消亡值](#值类别)外,也可以是[纯右值](#值类别)。
临时对象属性在绑定特定形式的参数时具有和 `P` 编码的附加信息类似的作用:
* 不具有临时对象属性的引用值作为[表达式](#表达式),在初始化临时对象的引用时被视为[左值](#值类别)(不影响其余属性)。
* 其它表达式的推断结果是右值。
带有临时对象属性的引用值和临时对象的引用值不同。特别地,作为引用属性值的临时对象属性允许在[运行时](#实现的执行阶段)作为对象的元数据[访问](#基本语义概念)以及跟随对象被跨过程传递,这无法被宿主语言支持,因为 `P` 表示的[静态类型](#类型系统和类型机制)信息不在函数外可用,仅在模板的类型参数 `P` 中而不在运行时可访问的元数据中(事实上,也不在对象的动态类型中)保留。关于其应用,参见[进一步讨论](#绑定临时对象)。
**注释**
因为宿主语言的引用折叠,以上 `P` 和 `P&&` 一致。
被绑定的这些对象可作为临时对象引用关联的[被引用对象](#一等引用)。
另见[绑定操作](#绑定操作)。
#### 临时对象的表示
作为一等对象的临时对象和其它一等对象表示方式一致。
非一等对象临时对象包括:
* 所有权可[被项独占](#项对象和关联对象所有权)而不能作为一等对象访问的对象。
* 具有[临时对象标签](#对象属性)的[项](#基本语义概念)作为内部表示的对象。
对临时标签对象决定的非一等对象,去除临时对象标签后,应具有一等对象表示。
在[项引用](#引用值的表示)以外的临时对象标签仅在[被绑定对象](#环境对象)上存在。
关于一等对象表示,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**原理**
至少在逻辑上,临时对象作为对象语言中不可见的对象和一等对象相同的宿主类型(即项)作为内部表示。因此,区分其内部表示并非通过宿主语言中的类型,而需通过运行时性质确定。
一些表示可能仅出现在临时对象中,而不是合法的一等对象表示。实现可据此进行一定运行时检查,以排除[互操作](#npla-互操作支持)或者错误实现中的误用。
对临时对象标签决定的非一等对象和一等对象表现之间的要求简化实现的一些操作,使[实质化](#值类别转换)不需依赖另行分配的资源。
临时对象的子对象不是临时对象,简化对临时对象的一些操作,也减少可能使临时对象标签扩散(到非预期的上下文影响一等对象表示)而误用。
关于被绑定对象的规则,参见[绑定临时对象](#绑定临时对象)中的原理。限制标签的使用范围以使之不和其它表示冲突。
关于实现中项的[宿主类型](#类型映射)和简化实现的例子,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**注释**
直接构成项的非一等对象可以是通过[源代码](../Terminology.zh-CN.md#程序设计语言)中的[外部表示](#表示)经[翻译](#实现的执行阶段)变换得到的具有内部表示的数据结构的非一等对象,参见上述实现中项的[宿主类型](#类型映射)。
### 表达式的类型
NPLA 的[类型系统](#类型系统和类型机制)使用[隐式类型](#类型标注);默认使用[潜在类型](#类型标注),保证[表达式的值具有类型](#npla-整体约定)。
NPLA 表达式的[类型](#类型)是表达式[求值结果](#基本语义概念)的类型。
[空求值](#求值性质)的求值结果要求未求值的[合式的](#翻译时正确性规则)表达式应具有和[语法分析](#实现的执行阶段)的实现的输出兼容的类型。
实现对特定的上下文的表达式可使用[类型推断](#类型标注)。由此确定的类型类似宿主语言的表达式的类型。
表达式具有[值类别](#值类别)。值类别的指派规则作为[定型规则](#类型系统和类型机制)是[类型系统的一部分](#类型系统和值类别)。但除非另行指定,值类别和 NPLA 及派生语言规则中描述的表达式的类型[正交](#正交性)。
关于语法分析的实现和其中处理的类型,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
**注释**
类型系统和 Scheme 及 Kernel 语言类似;除了表达式具有值类别这点和 Scheme 和 Kernel 不同而类似宿主语言。
表达式的类型和 \[R7RS] 的 expression type 无关,后者是[语法形式](#语法形式)的约定(在 \[R5RS] 和 \[R7RS] 中称为 form );因为存在[合并子](#合并子)作为[一等对象](#基本语义概念)的类型,不需要这种约定。
NPLA 中值类别和表达式的类型正交,这类似宿主语言。这简化了相关[类型规则](#类型)的描述。
## 生存期附加约定
和宿主语言不同,NPLA 子表达式的求值顺序可被不同的函数(特别允许显式指定对特定[操作数](#规范化中间表示)求值的[操作子](#合并子))中的求值调整,不需要特别约定。
NPLA 不存在宿主语言意义上的[完全表达式](#严格性),但在按宿主语言规则判断生存期时,使用[本机实现](#npla-互操作支持)的[函数合并](#合并子)视同宿主语言的完全表达式,其本机函数调用不引起函数内创建的对象的生存期被延长。
[临时对象](#临时对象)的[生存期](#npla-基础存储模型和对象模型)同时约束隐含的隐式宿主函数调用(如复制构造)。
为保证求值表达式取得的临时对象的[内存安全](#内存安全),函数合并同时满足以下规则:
* [操作符](#规范化中间表示)和未被求值的操作数的直接或间接子表达式关联的对象以及求值操作数的子表达式引入的临时对象的生存期结束的作用应不[后序](#规约顺序)于[活动调用](#函数调用)结束。
* 生存期起始和结束的顺序被确定(determined) 时,和对应所在的表达求值之间的[先序](#规约顺序)关系同构;否则,其顺序满足[非决定性有序](#规约顺序)关系。
**注释**
生存期的顺序约束确保引入临时对象时,其生存期不会任意地被[扩展](#生存期扩展)而超过[函数合并](#函数合并)的求值。
具体操作可在以上约束下指定被求值的操作数可能引入的临时对象的生存期。
## 尾上下文约定
NPLA 表达式[求值规约](#求值规约)中最后一个规约步骤中的上下文是[尾上下文](#上下文相关求值)。
尾上下文在 NPLA 中可满足一些附加的性质。
### 真尾规约
尾上下文涉及的存储在特定情况下满足调用消耗的空间有上界(即空间复杂度 O(1) )。
满足这种情况下的规约称为*真尾规约(proper tail reduction)* 。
### 尾调用和 PTC
在尾上下文规约的[调用](#函数调用)是*尾调用(tail call)* 。
以[真尾规约](#真尾规约)的实现尾调用允许具有不限定数量的(unbounded) [活动调用](#函数调用),称为 PTC(proper tail call ,真尾调用)。
PTC 占用[活动记录](#活动记录)满足真尾规约的上界的要求。
当宿主语言提供函数调用支持 PTC 时,可直接使用宿主语言的 PTC 调用,否则,需要使用其它替代实现机制确保 PTC 。
非对象语言的调用的上下文中,若被调用时间接使用,也仍需要保证 PTC 。
PTC 确保仅有一个[活动](#函数调用)的调用。不满足 PTC 的情形下,语言没有提供用户访问非[活动记录帧](#活动记录)资源的手段,因此可以认为是资源泄漏。但为简化语义规则,NPLA [不要求避免相关的弱内存泄漏](#求值和对象所有权)。
NPLA 不添加保证活动记录帧中保存引用,销毁活动记录的帧可能影响环境中的变量生存期而改变语义。
**注释** NPL 不保证一般对象存在[引用](#一等引用)。
因此,除非依赖本节中以下的规则,NPLA 不保证提供 PTC 支持;实现更一般的 PTC 依赖派生实现定义的附加规则。
除非另行指定,NPLA 要求至少在被[求值算法](#求值规约)中蕴含的[函数应用](#函数合并)的求值支持 PTC 。
为满足 PTC ,在[生存期附加约定](#生存期附加约定)的基础上,尾上下文内可以确定并调整对象生存期结束时机:
* 作为[临时对象](#临时对象)的[合并子](#合并子)及其参数可以[延长生存期](#生存期扩展)至多到尾上下文结束。
* 被证明不再需要之后引用的对象,或未被绑定到活动记录上的项中的对象,可以缩短生存期。
* 被延长生存期的对象生存期起始和结束的相对顺序保持不变。
* 被缩短生存期的不同对象生存期结束的相对顺序保持不变。
推论:被缩短生存期和延长生存期的对象的生存期结束的相对顺序保持不变。这由没有被调整生存期的对象与被调整生存期对象之间的生存期结束的[顺序关系](#尾上下文约定)的传递性保证。
延长临时对象生存期和宿主语言中允许扩展非完全表达式内的临时对象的效果类似,但条件不同。
**原理**
要求 PTC 主要用例是支持 [PTR](#ptr) 。相对 PTR ,PTC 更具有一般性,也适合对象语言[可观察行为](#状态和行为)以外的普遍性质。
**注释**
以上规则中被调整生存期的对象一般仅是参数和函数体内创建的对象。因此,不保证理论上允许的尾上下文的都满足 PTC 。一个例子是合并子中可以保存[动态环境](#函数和函数应用的求值环境),这个环境可能被递归的调用引用,而无法提前释放。
理论上 PTC 不要求延长生存期,仅要求特定情形下缩短生存期,且其它情形被释放的对象生存期不延长到尾上下文外。
允许延长生存期是[生存期附加约定](#生存期附加约定)的结果。
### PTR
PTC 的活动记录性质也在一般的递归规约时体现,被称为 PTR(proper tail recursion ,真尾递归)。
和 PTC 不同,PTR 要求的递归规约不一定是对象语言中的调用,以 PTR 描述时仅强调递归,不考虑尾上下文的适用性。
通过特定的保持语义等价的变换,对象语言可要求尾上下文作用于函数调用以外的上下文中(例如非[函数合并](#函数合并)的语法上下文)使用真尾规约实现。
除非派生实现另行指定,NPLA 对象语言不指定使函数调用以外的上下文作为尾上下文的要求;函数调用以外的尾上下文规约的仅可能用于实现的元语言中的[管理规约](#管理规约);非管理规约的真尾规约都用于尾调用。
此时,PTR 等价被递归调用的 PTC 。但由于支持 PTC 在非递归规约情形时也影响语言实现的一般构造,所以描述要求时一般不以 PTR 代替 PTC 或真尾规约。
关于 PTR 在 Scheme 为基础的形式模型,参见 \[Cl98] 。
PTR 的一个更激进的实现优化方式是 evlis tail recursion ,参见以下文献和参考资料:
* [Continuation-Based Program Transformation Strategies](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.83.8567&rep=rep1&type=pdf)
* \[Cl98]
* [Evlis tail recursion](https://www.akalin.com/evlis-tail-recursion)
因为 NPLA 使用[链接的环境](#环境对象),不支持实现其中更激进的 safe-for-space 保证。
**原理**
支持 PTR 使[重入](#过程)的函数调用保持较小的空间开销。这允许使用递归的函数调用代替尾上下文中特设的*循环(loop)* 和迭代语法实现等效的算法功能,满足[简单性](#简单性)和[通用性](#其它推论和比较)。
与[控制状态和支持一等状态的实现之间具有的偶然耦合](#一等状态)不同,使用支持 PTR 的递归函数调用代替循环的耦合可以是足够必要的:它排除了特设的循环语法的需要,同时也能满足实现自身的[简单性](#简单性),也因此可能更高效。
为支持[一等对象](#一等实体和一等对象),可被共享的环境一般不支持[平坦的表示](#环境对象)。
不具有 safe-for-space 保证时,实现对程序的闭包变换(closure conversion) 可能创建多余的[循环引用](#自引用数据结构和循环引用)且无法被运行时有效地分辨,而造成[资源泄漏](#资源泄漏)。通过明确支持这类保证的变换(如[这里描述的设计](http://flint.cs.yale.edu/flint/publications/escc.pdf) )可避免变换引起资源泄漏。
只要捕获自由变量的[静态环境](#函数和函数应用的求值环境)可被程序在创建函数时明确指定,safe-for-space 保证就不是必须的:避免在[语义规则约定](#生存期扩展)之外的生存期延长和资源泄漏是[用户程序](#程序实现)的责任;NPLA 程序可精确控制对象生存期,同时应当避免[循环引用](#npla-未定义行为)。
对 safe-for-space 保证的证明(如 \[Cl98] 和 [Closure Conversion Is Safe for Space](https://zoep.github.io/icfp2019.pdf) ),隐含要求和上述一等对象支持冲突的条件:环境中引用的对象总是可被复制的没有可观察行为的值。这实质上要求支持[共享引用](#共享引用)乃至可能要求一等对象都是[引用](#一等引用)。
因为使用链接的环境的要求,一般情形[不支持](#环境对象)对 safe-for-space 的变换。
即便允许类似的变换,这也仅保证不存在[不可达](#资源泄漏),仍然不保证资源被及时回收——[全局机制](#所有权抽象)可能具有不确定的延迟而造成的实时资源泄漏。
### TCO
TCO(Tail Call Optimization ,尾调用优化)是在以尾上下文规约时,允许减少修改规约状态的优化。
一般地,TCO 可重新排列规约过程中的被语义允许调整的副作用和其它不影响可观察行为的[状态](#状态和行为)的调用,减小空间开销。
TCO 的这种性质可以在宿主语言不支持 [PTC](#尾调用和-ptc) 时用于实现[对象语言的 PTC](#宿主语言中立) 。
**注释**
关于 TCO 和 PTC 的差异,另见[这里](https://groups.google.com/d/msg/comp.lang.lisp/AezzhxTliME/2Zsq7HUn_ssJ)。
#### 宿主语言中立
C++ 不要求实现支持 PTC ,也不保证支持 TCO 。因此,对象语言的 PTC 要求显式的 TCO 实现。
为可移植地支持 TCO ,NPLA 不依赖宿主语言中不可移植的[互操作](#npla-互操作支持)的活动记录(通常是体系结构相关的栈)。
**注释**
尾调用可避免尾上下文中非[嵌套调用安全](#嵌套调用安全)的情形的[宿主语言实现的未定义行为](#嵌套调用安全),但不保证非尾上下文中具有类似的性质。
#### TCO 实现策略概述
TCO 包括以下形式:
* 静态 TCO :实现时替换宿主语言中不保证满足 PTC 的构造为满足 PTC 的构造。
* 动态 TCO :运行时调整直接或间接表示对象语言构造的数据结构和状态,使状态占用的空间复杂度满足 PTC 要求。
静态 TCO 也适合非对象语言的调用的[上下文](#尾调用和-ptc)。
不依赖宿主语言特性的静态 TCO 包括以下形式:
* 替换宿主语言实现中的不保证满足 PTC 的递归调用为满足 PTC 的结构(如循环结构),包括直接编码和自动的变换(transformation) ,称为宿主(host) TCO 。
* 替换不满足 PTC 的对象语言原语为满足 PTC 的表达形式,称为目标(target) TCO 。
不依赖宿主语言特性的动态 TCO 包括以下形式:
* 通过合并不同[活动调用](#函数调用)中活动记录占用的冗余状态,减少宿主语言的活动调用同时占用的总空间,称为 TCM(Tail Call Merging ,尾调用合并)。
* 引入具有便于操作[控制作用](#基本语义概念)的构造,同时作为一些其它优化的基础,以消除部分活动记录状态的分配,称为 TCE(Tail Call Elimination ,尾调用消除)。
**注释**
本节的内容不影响对象语言的语义,但可能影响[互操作](#npla-互操作支持)的接口兼容性和实现质量。
关于对实现的具体影响,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
## NPLA 数学功能
NPLA 数学功能(模块 NPLAMath )提供数学功能和相关支持。
关于 NPLA 数学功能的规格说明的其它部分,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
### 数值类型
[NPLA 数值](#附加功能)是 NPLA 对数学意义上的*数(number)* 的建模。
被建模的数是 NPLA 数值的*真值(true value)* 。
NPLA 数值的集合到真值的集合的映射是满射;除此之外,也存在不被 NPLA 数值建模的数,这些数可能被 NPLAMath 未来的版本支持作为真值。
除非另行指定,NPLA 数值的行为由对应的真值的数学含义决定。
基于宿主语言的类型系统,NPLA 支持以下按数值范围从小到大排列的本机整数和浮点数作为[宿主类型](#类型映射):
* `signed char`
* `unsigned char`
* `signed short`
* `unsigned short`
* `int`
* `unsigned`
* `long`
* `unsigned long`
* `long long`
* `unsigned long long`
* `float`
* `double`
* `long double`
文法表示:
支持的数值类型以 `` 表示,具有以下表示数值的[子类型](#类型序):
* `` :复数。
* `` :实数。
* `` :有理数。
* `` :整数。
其子类型由数学定义蕴含,即以上类型中,后者依次是前者的子类型。
当前所有数值都是 `` ,因此暂时没有针对 `` 值是否属于 `` 和 `` 的类型检查。
和数学意义上的实数不同,`` 也包含以下可带符号(sign) 的*特殊值(special value)* :
* 无限大值。
* [NaN(not a number)](https://zh.wikipedia.org/zh-cn/NaN) 值。
对应地,`` 也包含实部和/或虚部是上述特殊值的特殊值。
**注释** 当前所有复数都是实数,因此虚部总是 0 。
根据数值是否完全保留真值在数学上的唯一性即*精确性(exactness)* ,数值分为*精确数(exact number)* 和*不精确数(inexact number)* 。
精确数和对应的真值总是相等;不精确数和真值不严格相等。
有限的不精确数的偏离程度可通过实数描述,即(绝对)*误差(error)* 。精确数的误差恒等于 0 。
除非另行指定,特定不精确数的具体的误差是未指定的。
数值的绝对*精度(precision)* 是其内部表示蕴含的误差的上界的倒数。对确定使用进位制的表示,精度也指精确表示的数值位数。
数值的*任意精度(arbitrary precision)* 指除[实现环境的可用资源(一般即存储空间)限制](#资源可用性基本约定)外,不限制精度。
**注释** 为支持更多数学上有意义的真值,未来可能引入其它类型来表示任意精度的整数、有理数及数学意义上的扩展(如复数和四元数)。
数值的内部表示中能以实数描述的度量应至少具有整数数量级精度,即误差不大于 1 。
精确数和不精确数在数值上可能相等,而[类型不同](#类型等价性)。
宿主类型中的本机整数和浮点类型是数值类型的子类型,分别称为 fixnum 和 flonum 。这些类型在项的内部表示预期直接占据本机存储而不需要动态分配。
Fixnum 总是精确数;flonum 总是不精确数。
**注释** 当前实现中,所有数值是 fixnum 或 flonum 之一。两者分别是宿主的整数类型(排除字符和 `bool` 类型)以及浮点数类型。
Flonum 支持带符号的无限大值以及 NaN 值作为特殊值。其它值都是有限值。特殊值可能具有不唯一的内部表示,但和有限值的表示都不同。
Flonum 中可存在小的非零数,可能和其它数值不同的内部表示而更容易在计算中损失精度,即非规格化(denormalized) 数值。
整数值的数值具有整数类型。这包括所有的 fixnum ,以及 flonum 中是整数的数值。这不和宿主类型直接对应。
**注释** 一个 flonum 是整数,当且仅当它的值取整后结果和原值相等。这里的取整使用可使用任意的舍入。\[R7RS] 对不精确数有类似的定义(仅使用 round )。
对 fixnum ,`+0` 和 `-0` 是相等的数值。Flonum 不同符号的零值在值的表示中可以不同,但在数学意义上相等,表示同一个数。实现中的其它和具体表示无关的[等价谓词](#实体的等价性)是否表现这种不同是未指定的。
**注释**
NPLA 的不同的[名义](#类型等价性)数值类型的集合到宿主类型的集合的[类型映射](#类型映射)是满射,即本节指定的宿主类型总是关联至少一个能表示它的值的 NPLA 数值类型。
同时,NPLA 数值类型可以映射到其它类型,特别地,NPLA 整数的类型映射目标是宿主语言整数类型和包含整数值的非典型[宿主类型](#类型映射)的并。所以,NPLA 数值整体的类型映射不构成简单的一对多或多对多关系。
特殊值同 ISO/IEC 10967–1 (LIA–1) 定义,引用 \[IEC 60559] (IEEE-754) 的具体值,仅适用于浮点数。
无限大值在数学上属于*超实数(hyperreal number)* ,在浮点数实现中属于*扩展实数(extended real number)* 。
无限大值的符号在数学意义上是必要的,因此也被要求区分。
精确性、fixnum 和 flonum 等区分同 \[R6RS] ,但具体实现要求不尽相同。
NaN 不是数学意义上的数,表示特定的没有数学定义的计算结果。NaN 和任何数值比较总是不相等。
NPLA 的宿主语言支持的 NaN 值带有符号。不是所有实现都区分符号,如 \[ECMAScript] 。
整数精度外的数量级精度仅在确定使用的进位制底数时和绝对精度可比较,因此常用于描述特定实现的内部表示(例如,\[ISO C] 定义的浮点数精度即有效数字的位数)。但是,不比较具体大小时,有限的数量级精度和绝对精度性质可以一致,这种上下文可不区分两者。
大多数不精确数的浮点表示的高效实现使用底数 2 。
参照 \[IEC 60559] ,浮点格式的无限大值和有限数值是浮点数;NaN 不是浮点数。两者统称为*浮点数据(floating-point datum)* 。
宿主类型中实现为 \[IEC 60559] 的*非规格化(denormalized)* 浮点数是具有不唯一的内部表示的小的非零数值。使用 IEEE-754 2008 以来的定义,这些数是*非规格(subnormal)* 数。
和 \[RnRK] 不同,本文档没有指定可选的模块,也没有指定精确的 ±∞ 值。
和 \[RnRK] 不同,不精确数不指定边界和*主值(primary value)* ,NaN 值被显式提供而不是唯一的 `#undefined` 值,非规格数不作为 `#undefined` 。这同时不要求在任意的操作中检查 `#undefined` 值并[引起错误](#错误)。
尽管容易损失精度,区分不同的非规格数的数值仍然有意义。同时,也避免和 NaN 用以表示数学上未定义操作的结果(如 0 除以 0 )引起混淆。
数值相等和一般对象相等可使用不同的等价谓词。和一般对象比较不同,数值相等比较可对参数要求数值类型,否则引起错误。两者比较结果可能也不总是相同。如 Scheme 的 `=` 和 `eqv?` 以及 Kernel 的 `=?` 和 `equal?` 。
### 数值操作约定
在对象语言中,数值操作是可使用数值作为算法输入的值的操作。NPLA1 提供本机 API 支持这些操作的实现。
数值操作数和非数值操作数分别是具有和不具有数值类型的操作数。
数值操作蕴含对应的数值计算,接受至少一个数值或非数值操作数,预期得到[计算结果](#过程)。
**注释** 非预期情形可[引起错误](#错误)。
其中,计算结果依赖影响计算结果的操作数,并依赖至少一个数值操作数。
除非另行指定:
* 在数学上有意义的前提下,数值操作同时支持以上尽可能多的数值类型的操作数。
* 数值操作对预期的数值操作数进行[类型检查](#类型检查),失败时出错。
* 数值操作不区分数值操作数中对应的真值相等的精确数或不精确数。
* 可假定数值操作数和计算过程中不出现 [SNaN(signaling NaN) (en-US)](https://en.wikipedia.org/wiki/NaN#Signaling_NaN) 值。
* 若作为操作数的精确数决定计算结果在数学上未定义,则引起错误。
* 不精确数计算中的舍入方式未指定。
* 若计算结果是数值,则:
* 若被计算结果依赖的任一操作数中具有 NaN 值,则依赖这个操作数的数值操作结果也是 NaN 值。
* 输出的类型的值域能表示操作结果;除操作的语义和本节的其它规则蕴含外,具体类型未指定。
* 若作为操作数的精确数决定的计算结果是不精确数表示的有限数值,则这个不精确数应是所有相同内部表示的数值中和结果的真值误差最小的数值。
* 对数学上封闭的计算,结果具有不超过所有数值操作数范围的数值类型。
* 除非不能在结果类型中表示计算结果的范围:
* 若数值操作的所有数值操作数都是精确数,结果不是不精确数。
* 数值操作的实现不损失按数学定义得到的中间结果的[精度](#数值类型);结果的[误差](#数值类型)仅来自其依赖的数值操作数引入的累积误差。
* 若计算结果是不精确数,则:
* 若计算结果是小于最小可唯一表示的 `` 值,则对应的数值操作结果是不精确数 0 。
* 计算结果中真值等于 0 的数值以及 NaN 值的符号是未指定的。
* 若计算结果中无限大数值不能通过数学上有意义的方式确定符号,则对应的数值操作结果是无限大值或 NaN 之一,具体选择未指定。
* 数值的宿主类型未指定。
**原理**
因为典型的高效实现实现依赖[外部环境](#略称)对浮点数的支持,设计策略以保持[互操作](#npla-互操作支持)的便利性相关。
* NPLAMath 实现不访问 SNaN 值,也不需要访问宿主语言的浮点环境,但不假设总是使用默认浮点环境。
* 这不阻止和使用 SNaN 的[本机实现](#npla-互操作支持)的程序链接和调用,这有助于保持互操作性。
* NPLAMath 实现不保证检查访问浮点环境的副作用是否存在。若互操作需要改变浮点环境,应避免破坏实现的假设。
* 不依赖零值的符号、NaN 的符号以及 SNaN 的处理和许多[宿主实现](#需求概述)的默认情形一致而能简化一般的实现,如:
* [GCC](https://gcc.gnu.org/wiki/FloatingPointMath) 。
* [Microsoft VC++](https://docs.microsoft.com/cpp/build/reference/fp-specify-floating-point-behavior) 。
* 不要求使用 GCC 时启用 `-ffloat-store` 。Microsoft VC++ 默认的 `/fp:precise` 的类似语义也不被依赖。
**注释**
数值操作可能允许非数值的操作数,这些操作数也可被计算结果依赖。
因为 [flonum](#数值类型) 能表示所有实数数值范围,所以实数范围以内的操作不会引入操作数以外的其它 flonum 类型。
数值操作抛出异常的要求不一定在每一个实现数值操作的 [API](../Terminology.zh-CN.md#程序设计语言) 中蕴含,因为这些 API 不一定是数值操作的完整实现。
抛出异常和[宿主环境](#嵌入宿主语言实现)的异常和浮点异常没有直接关联。
若数值操作指定非数值计算结果(如[布尔值](#基本语义概念))或者不能表示的 NaN 值的计算结果,则即使依赖 NaN 数值操作数,也不是 NaN 值。
和 \[RnRK] 不同,数值操作不支持不同的全局模式。
特定情形下,精确数可能替换计算结果中的不精确数:
* 当按数学定义能被精确表示时,计算结果可以是符合要求的任意一个类型的精确数。
* 否则,当实现能证明不精确数值足够小到不足以影响结果的表示,且存在真值相等的可用的精确数时,可使用这个精确数代替不精确数。
* 当前实现没有这类证明机制。
IEEE-754 使用*渐进下溢(gradual underflow)* ,使零值和相邻的非零浮点数的真值之差不会显著大于其它两个浮点数真值的差,而使小的非零浮点数之间的差不等于零。这体现了支持[非规格化数](#数值操作约定)的实际作用。但一般数值计算仍需要累积误差。
虽然可能影响结果,浮点数实现的内部状态(如舍入模式)的访问不被直接支持。
浮点数 0 和 0 之差的符号可能取决于舍入模式。数值操作一般不保证结果 0 的符号,但以依赖[表示的形式](#数值表示)仍可确定符号。
除满足必要的精度要求的前提外,互操作以外目的的数值的宿主类型的具体选择在维持计算正确性的意义上通常不重要,因此默认不要求指定。
对特殊值,因为 \[RnRS] 只要求 `.0` 后缀的特殊值字面量,需保持兼容时,程序可只使用这些形式的字面量。为此,实现可使用带有 `.0` 后缀特殊值的数值字面量的对应的宿主类型,以减少潜在的可移植问题。
关于派生实现支持的数值字面量,参见 [NPLA1 数值字面量](#npla1-数值字面量)。
### 数值表示
支持解析的数值以字符串作为外部表示。作为字面量时,构成[数值字面量的词法](#npla-扩展字面量)。
数值的外部表示和内部表示应支持*往返(round-trip)* 转换,即转换的内部或者外部表示输出可被输入接受。
往返转换中,精确数转换保持任意(无限)精度;不精确数经有限次转换不继续损失精度。
**注释** 即便损失精度,也应总是满足结果至少不低于[整数精度](#数值类型)。
支持的外部表示和对应的含义具体包括:
* 数值的外部表示中起始的一个 `+` 或 `-` 字符指定符号。
* 这可能是可选的。若符合规则的数值字面量没有指定符号,则隐含为 `+` 。
* **注释** 不精确数可能在内部表示支持不同符号的[零值](#数值类型)。
* 以下优先匹配较长的模式。
* 匹配[正则表达式](https://zh.wikipedia.org/zh-cn/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F) `(+|-)?[0-9]+` :十进制整数值。
* 不论符号,当前精确数数值字面量默认都具有[宿主类型](#类型映射) `int` ,除非其绝对值太大而无法被表示,使用其它类型代替。
* 除非精确数字面量的数值超过所有 [fixnum](#数值类型) 的可表示范围,都具有 fixnum 值。
* 除非精确数字面量的数值超过所有支持的精确数的可表示范围,都是精确数。
* **注释** 当前精确数的表示范围是 fixnum 中宿主类型的值域的并集,因此超过 fixnum 可表示范围的数值不是精确数。
* 匹配正则表达式 `(+|-)?[0-9]+\.[0-9]*` 或 `(+|-)?[0-9]+(\.[0-9]*)?(E|e|S|s|F|f|D|d|L|l)(+|-)?[0-9]+` :十进制不精确数数值。
* 不精确数数值字面量的解析使用未指定的浮点数舍入模式,其[误差](#数值类型)不大于最后一个在规格化范围内表示的十进制小数位为 1 时的绝对值的真值大小。
* 解析不精确数外部表示得到的真值和内部表示可具有误差。
* **注释** 误差和具体宿主语言支持相关,通常以任意可能符合宿主语言要求的舍入模式下的最大值计。
* 第一种形式是直接记法。
* 第二种形式是[科学记数法(scientific notation)](https://zh.wikipedia.org/zh-cn/%E7%A7%91%E5%AD%A6%E8%AE%B0%E6%95%B0%E6%B3%95) ,在指示指数的指数字母前后匹配的数字序列分别是有效数字(significand) 和指数(exponent) 。
* 指数字母表示作为 [flonum](#数值类型) 的不同精度:
* `E` 或 `e` :默认精度。
* `S` 或 `s` :短(short) 精度。
* `F` 或 `f` :单精度(float) 。
* `D` 或 `D` :双精度(double) 。
* `L` 或 `l` :长精度(long) 。
* 同组的字母含义等价。以上精度中,默认精度不低于双精度,其它精度依次不低于之前的一个。
* 精度可影响内部存储的宿主类型。
* 若字面量指定的数值小于或大于使用的类型的数值表示范围,则值为对应类型具有相同符号的零值或[无限大值](#数值类型)。
* 匹配正则表达式 `(+|-)(inf|nan)\.(0|f|t)` :带符号的 flonum 特殊值。
* 其中,`inf` 指定无限大值,`nan` 指定 NaN 值。
* 后缀指定精度:
* `0` :默认精度。
* `f` :单精度。
* `t` :扩展(extended) 精度。
* 以上精度中,默认精度不低于双精度,扩展精度不低于默认精度。
除非派生实现另行指定,以上要求外的数值的子类型和内部表示未指定。
**原理**
浮点数解析存在不同精度的算法。
若以二进制浮点数和经过舍入的十进制表示相互转换不损失精度为前提,宿主语言的 [`std::numeric_limits`](http://eel.is/c++draft/numeric.limits) 的 [`max_digits10`](http://eel.is/c++draft/numeric.limits.members#14) 位十进制数字足够表示。
(对 \[IEC 60559] 的二进制浮点数情形的证明参见[这里](https://www.itu.dk/~sestoft/bachelor/IEEE754_article.pdf)的 Theorem 15 。)
但是,对任意有效输入的结果误差都不大于 1 [ULP(unit in the last place)](https://zh.wikipedia.org/zh-cn/%E6%9C%80%E5%90%8E%E4%B8%80%E4%BD%8D%E4%B8%8A%E7%9A%84%E5%8D%95%E4%BD%8D%E5%80%BC) 的不经舍入的值完全精确值(full precision) 的精确解析算法,对实现的要求较高,且性能可能明显较低,故不作要求。
(对 \[IEC 60559] 的二进制浮点数的情形,需要数十倍的中间存储,参见[这里](https://stackoverflow.com/questions/554063/62542806)。)
和宿主语言的 `std::strtod` 不同,允许使用宿主语言中的任意浮点数舍入模式,而不要求不同浮点数舍入模式下的结果一致性。
浮点数精度的 `float` 和 `double` 在典型实现中的内部表示格式同 \[IEC 60559] 的二进制的单精度和双精度浮点数。
**注释**
串模式 `(+|-)` 表示带有可选前缀符号(仅限一个),影响值的数值。
同 klisp 而不同于 \[RnRS] 的字面量词法,小数点不能出现在词素中符号以外的第一个字符;但 klisp 的 `string->number` 没有这个限制。
同 \[RnRS] 而不同于 klisp(两者包括字面量词法和 `string->number` ),小数点允许出现在词素的结尾。
当前不精确数数值都具有宿主类型 `double` 。即便 `long double` 可能具有更大的数值范围,也不能通过解析数值表示直接取得。
类似地,\[Racket] 默认不启用 [extflonum](https://docs.racket-lang.org/reference/extflonums.html)。关于数值操作也类似,参见[数值操作约定](#数值操作约定)。
当前允许在[宿主值](#类型映射)不能完全存储不精确数的字面量数字时,解析十进制不精确数字面量存储的值可能和字面量的数值的真值之间具有超过 1ULP 的误差。这可能影响和精确数之间的比较。
当前实现使用四舍五入。
关于宿主语言中 [`std::strtod`](http://www.eel.is/c++draft/cstdlib.syn#lib:strtod)(同 \[ISO C] 标准库的 `strtod` )舍入要求的一些问题,参见:
* [Incorrectly Rounded Conversions in GCC and GLIBC](https://www.exploringbinary.com/incorrectly-rounded-conversions-in-gcc-and-glibc/)
* [Incorrect rounding in `strtod()`](https://www.sourceware.org/bugzilla/show_bug.cgi?id=3479)
数值字面量的词法同 \[RnRS] 的一个子集。
\[RnRS] 指出实现可能允许用户修改不同的默认精度。这指出精度不是固定的,但不是实现要求。
\[RnRK] 的有限数的对应子集接近 \[RnRS] 的设计,但没有明确指定字面量词法规则。其中对精度的表述略有不同:
* 没有指定大写字母。
* 指定 `s` 、`f` 、`d` 和 `l` 的精度递增,没有显式允许不同的精度映射到相同的内部格式。
* 没有显式允许用户指定的默认精度。
但是,SINK 和 klisp 实际上都不符合前两点,而更符合 Scheme 的实现。\[RnRK] 在此可能不完善或表述有误。
当前 klisp 的实现不允许 `E` 和 `e` 之前没有小数点,且在存在 `E` 或 `e` 时省略之后的指数的任意部分,和 SINK 以及 \[RnRS] 都不同。本设计遵循后者。
当前 NPLAMath 精度对应的宿主类型指派如下:
* 非特殊值:
* `e` :同 `d` 。
* `s` :同 `f` 。
* `f` :`float` 。
* `d` :`double` 。
* `l` :`long double` 。
* 特殊值:
* `0` :`double` 。
* `f` :`float` 。
* `t` :`long double` 。
指定特殊值的精度的[词素](#基本词法构造)语法兼容 \[Racket] 。
# NPLA1 核心语言
NPL 是独立设计的,但其派生语言和其它一些语言有类似之处;这些语言和 NPL 方言之间并不具有派生关系。但为简化描述,部分地引用这些现有[语言规范](../Terminology.zh-CN.md#非自指)中的描述,仅强调其中的不同。
NPLA1 符合 NPL 和 NPLA 的语言规则,其[实现环境](../Terminology.zh-CN.md#程序设计语言)还应提供本章起的其它程序接口。
互操作中的一些接口处理的值可约定[宿主类型](#类型映射)。但这些类型不一定在[对象语言](../Terminology.zh-CN.md#程序设计语言)层次上稳定,可能在之后的版本变化。稳定性由具体实现提供的附加规则(若存在)保证。
NPLA1 和 Kernel 语言(即 \[RnRK] )的特性设计(如 [vau](#vau-抽象)和作为[一等对象](#基本语义概念)的环境表达[过程抽象](#过程))有很多类似之处,因此许多概念是通用的;但从[设计哲学](#领域设计原则)到本章介绍的各个细节(如[默认求值规则](#对象语言求值算法))都存在深刻的差异。
部分名称指定的操作和 \[RnRS] 或 [klisp](http://klisp.org/docs/index.html) 指定的类似。
以下章节主要介绍和 Kernel 约定不同的设计。各节的通用约定不再在之后的各个接口单独说明。
## NPLA1 对象语言约定
NPLA1 仅使用[宿主语言](#npla-实现环境)的[类型](#类型)和[值](#基本语义概念)作为在对象语言可表达的[状态](#基本语义概念)。
在 [NPLA](#npla) 的基础上,NPLA1 要求对象语言支持以一等对象作为表达式并被[求值](#基本语义概念)。
[类型等价性](#类型等价性)基于[类型映射](#类型映射)及其实现,由 \[ISO C++] 的语义规则定义。
值等价性由[宿主环境](#嵌入宿主语言实现)的 `==` 表达式的结果定义。
除非另行指定,所有类型的[外部表示](#表示)都是允许从作为内部表示的[项节点](#项的子对象)确定的同[宿主类型](#类型映射)的空字符结尾的字符串(即 \[ISO C++] 的 [NTCTS](https://eel.is/c++draft/defns.ntcts) )。
关于作为表达式的求值和类型映射的实现,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
### 标识符附加规则
当前仅支持[标识符](#基本词法构造)作为[名称](#名称)。
部分名称是[保留名称](#npla-名称和字面量求值):含有 `$$` 的名称保留给宿主交互使用;含有 `__` 的名称保留给 NPLA1 实现。
在 [NPLA 规则](#npla-整体约定)的基础上,在[内部表示](#表示)中显式使用保留给实现的标识符的程序[行为未定义](#npla-未定义行为)。
**注释** 这包含在源代码以外的中间表示使用的情形,但不包含作为用户输入的数据。
## NPLA1 互操作约定
基本规则参见 [NPLA 互操作支持](#npla-互操作支持)。
非 NPLA1 实现提供的类型的宿主 `==` 操作不要求支持[嵌套调用安全](#嵌套调用安全)。
作为 NPLA1 嵌套调用安全的约定的扩展,若存在 == 操作不支持嵌套调用安全的类型,具体类型由派生实现的定义。
对象语言操作和互操作不修改对象语言中已经可见的[一等环境](#npla-环境)的[父环境](#npla-环境)。
**原理**
NPLA1 中提供的类型仍需要支持嵌套调用安全,以满足嵌套调用安全的约定中的要求。
关于 NPLA1 嵌套调用安全的具体约定和其它实现原理,参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
避免修改已在对象语言可访问的一定对象的父环境符合同 \[RnRK] 的环境[封装性](#封装)对对象语言的要求。这允许实现假定仅在有限的上下文中父环境可修改,而减少优化实现的难度。
## NPLA1 程序实现
本章指定 NPLA1 对象语言的[核心语言特性](#程序实现)。包含[库特性](#程序实现)的其它设计参见 [NPLA1 参照实现环境](#npla1-参照实现环境)。
**原理**
一般的语言能支持不同实现形式的库,包括[源程序](../Terminology.zh-CN.md#程序设计语言)和其它无法判断是否和特定源程序关联的翻译后的程序。
复用这些程序时,可能需要根据不同的形式而分别处理:源代码被读取和[求值](#基本语义概念)而加载,而其它格式的翻译形式可能直接映射存储后经特定的检查即被加载。
但是在可复用的意义上,这些不同的形式是一致的,都视为库。
**注释**
和 \[RnRK] 不同,库不限定其实现形式。\[RnRK] 指定的库实质上是可使用对象语言派生实现的库。
典型的静态语言不保证程序执行时能对源程序进行翻译,因此加载程序的限制通常更大,可能无法处理源程序形式的库而首先需要分离翻译为其它格式。NPL 一般不具有这个限制。
关于对象语言的[派生实现](#略称),参见 YSLib 项目文档 [`doc/NPL.txt`](https://osdn.net/projects/yslib/scm/hg/YSLib/blobs/tip/doc/NPL.txt) 。
### NPLA1 程序外部环境
基于 [NPLA 整体约定](#npla-整体约定),由 [NPL-EMA](#嵌入宿主语言实现) ,NPLA 的实现不假定存在多线程执行环境。
但是,宿主语言可支持多线程环境执行,[可引起宿主语言的未定义行为](#npla-并发访问)。
作为 NPLA 的派生,NPLA1 对象语言程序也具有相同的性质,除非另行指定需要和[外部环境](../Terminology.zh-CN.md#程序设计语言)交互的特定操作,不需要假定 NPLA1 引入存在多线程执行环境。
### 附加元数据
NPLA1 实现可提供和[实现环境](../Terminology.zh-CN.md#规范)或具体 NPLA 对象关联的附加的资源,用于提供程序运行时可得到的附加信息,如源代码位置。
是否存在这些*附加元数据(extra metadata)* 和附加元数据的具体内容可影响特定的行为。
**注释** 如符合[诊断](#诊断)中要求的实现的具体行为。
这些影响是未指定的,但除 NPLA1 程序直接依赖具体数据而进行的操作外,不应影响程序的其它语义(例如,引起程序终止)。
### NPLA1 扩展支持
本章中除[循环引用](#循环引用)的限制外,不支持的特性可能会在之后的实现中扩展并支持。
### NPLA1 未定义行为
NPLA1 对象语言程序中的[未定义行为](../Terminology.zh-CN.md#程序设计语言)包括 [NPLA 未定义行为](#npla-未定义行为)和以下扩展 NPLA 未定义行为:
* 特定情形下[访问被修改的环境中绑定的对象](#npla1-环境)。
* 特定情形下违反对环境的使用要求。
* **注释** 参见[环境的稳定性](#环境的稳定性)。
* 特定情形下违反对外部实现环境假定的使用要求。
* **注释** 参见[模块的初始化和加载](#模块的初始化和加载)。注意虽然可能通过环境实现,但这在接口的意义上独立于环境的稳定性。
* NPLA1 库约定的未定义行为。
* **注释** 有些未定义行为不需要被显式约定,例如不支持的并发访问[可引起宿主语言的未定义行为](#npla1-程序外部环境),属于 NPLA 未定义行为。
派生语言可约定其它未定义行为。
**原理**
扩展 NPLA 未定义行为可提供更严格的要求使实现更简化。
关于环境的一些未定义行为可视为违反[内存安全](#内存安全),而不需要单独实现。
## 接口文法约定
为描述对象语言规则和程序接口,本节约定[文法](../Terminology.zh-CN.md#程序设计语言)形式。
**注释** 这仅用于描述接口,不依赖 NPL 语言的[基本文法](#基本文法)。
元语言文法:
```xbnf
::= <
::= >
::=
::= .
::= ? | ...
::= | |
::= *
::= (
::= )