define TCS language and prove stage0 execution slice

This commit is contained in:
冰朔 2026-08-21 16:57:25 +08:00
commit 3959ae4270
31 changed files with 2449 additions and 5 deletions

284
bootstrap/tcs-stage0/Cargo.lock generated Normal file
View file

@ -0,0 +1,284 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tcs-stage0"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"sha2",
"tempfile",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"

View file

@ -0,0 +1,15 @@
[package]
name = "tcs-stage0"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
description = "Replaceable Stage-0 ignition parser/compiler for TCS; never native authority"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
[dev-dependencies]
tempfile = "3"

View file

@ -0,0 +1,805 @@
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use sha2::{Digest, Sha256};
use std::{
collections::BTreeMap,
fs,
path::{Component, Path, PathBuf},
};
pub const COMPILER_ID: &str = "TCS-STAGE0-IGNITION-0001";
pub const COMPILER_STATE: &str = "FOREIGN_HOST_BOOTSTRAP_SEED_NOT_SELF_HOSTED";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum TcsValue {
Null,
Bool(bool),
Int(i64),
Text(String),
List(Vec<TcsValue>),
Object(BTreeMap<String, TcsValue>),
}
impl TcsValue {
fn object(&self, field: &str) -> Result<&BTreeMap<String, TcsValue>, TcsError> {
match self {
Self::Object(value) => Ok(value),
_ => Err(TcsError::new(
"TCS-E2001",
format!("{field} must be an object"),
)),
}
}
fn text(&self, field: &str) -> Result<&str, TcsError> {
match self {
Self::Text(value) => Ok(value),
_ => Err(TcsError::new("TCS-E2001", format!("{field} must be text"))),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TcsDocument {
pub language_version: String,
pub declaration_kind: String,
pub declaration_id: String,
pub body: BTreeMap<String, TcsValue>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TcsError {
pub code: &'static str,
pub message: String,
pub line: usize,
pub column: usize,
}
impl TcsError {
fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
line: 0,
column: 0,
}
}
fn at(mut self, line: usize, column: usize) -> Self {
self.line = line;
self.column = column;
self
}
}
impl std::fmt::Display for TcsError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.line == 0 {
write!(formatter, "{}: {}", self.code, self.message)
} else {
write!(
formatter,
"{} at {}:{}: {}",
self.code, self.line, self.column, self.message
)
}
}
}
impl std::error::Error for TcsError {}
#[derive(Debug, Clone, PartialEq)]
enum TokenKind {
Identifier(String),
Text(String),
Int(i64),
Bool(bool),
Null,
LBrace,
RBrace,
LBracket,
RBracket,
Less,
Greater,
Colon,
Equal,
Comma,
Semicolon,
Dot,
Eof,
}
#[derive(Debug, Clone)]
struct Token {
kind: TokenKind,
line: usize,
column: usize,
}
struct Lexer<'a> {
chars: Vec<char>,
index: usize,
line: usize,
column: usize,
_source: &'a str,
}
impl<'a> Lexer<'a> {
fn new(source: &'a str) -> Result<Self, TcsError> {
if source.starts_with('\u{feff}') {
return Err(TcsError::new("TCS-E0001", "UTF-8 BOM is forbidden"));
}
Ok(Self {
chars: source.chars().collect(),
index: 0,
line: 1,
column: 1,
_source: source,
})
}
fn peek(&self) -> Option<char> {
self.chars.get(self.index).copied()
}
fn peek_next(&self) -> Option<char> {
self.chars.get(self.index + 1).copied()
}
fn bump(&mut self) -> Option<char> {
let value = self.peek()?;
self.index += 1;
if value == '\n' {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
Some(value)
}
fn skip_space_and_comments(&mut self) -> Result<(), TcsError> {
loop {
while self.peek().is_some_and(char::is_whitespace) {
self.bump();
}
if self.peek() == Some('/') && self.peek_next() == Some('/') {
while self.peek().is_some_and(|value| value != '\n') {
self.bump();
}
continue;
}
if self.peek() == Some('/') && self.peek_next() == Some('*') {
let line = self.line;
let column = self.column;
self.bump();
self.bump();
loop {
match (self.peek(), self.peek_next()) {
(Some('*'), Some('/')) => {
self.bump();
self.bump();
break;
}
(Some(_), _) => {
self.bump();
}
(None, _) => {
return Err(TcsError::new("TCS-E0002", "unterminated block comment")
.at(line, column));
}
}
}
continue;
}
return Ok(());
}
}
fn token(&mut self) -> Result<Token, TcsError> {
self.skip_space_and_comments()?;
let line = self.line;
let column = self.column;
let Some(value) = self.peek() else {
return Ok(Token {
kind: TokenKind::Eof,
line,
column,
});
};
let punctuation = match value {
'{' => Some(TokenKind::LBrace),
'}' => Some(TokenKind::RBrace),
'[' => Some(TokenKind::LBracket),
']' => Some(TokenKind::RBracket),
'<' => Some(TokenKind::Less),
'>' => Some(TokenKind::Greater),
':' => Some(TokenKind::Colon),
'=' => Some(TokenKind::Equal),
',' => Some(TokenKind::Comma),
';' => Some(TokenKind::Semicolon),
'.' => Some(TokenKind::Dot),
_ => None,
};
if let Some(kind) = punctuation {
self.bump();
return Ok(Token { kind, line, column });
}
if value == '"' {
return self.string_token(line, column);
}
if value == '-' || value.is_ascii_digit() {
return self.integer_token(line, column);
}
if value == '_' || value.is_alphabetic() {
return Ok(self.identifier_token(line, column));
}
Err(TcsError::new("TCS-E1001", format!("unexpected character {value:?}")).at(line, column))
}
fn identifier_token(&mut self, line: usize, column: usize) -> Token {
let mut value = String::new();
while self.peek().is_some_and(|character| {
character.is_alphanumeric() || matches!(character, '_' | '-' | '.' | '/' | ':' | '@')
}) {
value.push(self.bump().expect("peeked character"));
}
let kind = match value.as_str() {
"true" => TokenKind::Bool(true),
"false" => TokenKind::Bool(false),
"null" => TokenKind::Null,
_ => TokenKind::Identifier(value),
};
Token { kind, line, column }
}
fn integer_token(&mut self, line: usize, column: usize) -> Result<Token, TcsError> {
let mut value = String::new();
if self.peek() == Some('-') {
value.push(self.bump().expect("minus"));
}
while self
.peek()
.is_some_and(|character| character.is_ascii_digit())
{
value.push(self.bump().expect("digit"));
}
if value == "-" {
return Err(TcsError::new("TCS-E1001", "minus must precede digits").at(line, column));
}
let parsed = value
.parse::<i64>()
.map_err(|_| TcsError::new("TCS-E2001", "integer out of range").at(line, column))?;
Ok(Token {
kind: TokenKind::Int(parsed),
line,
column,
})
}
fn string_token(&mut self, line: usize, column: usize) -> Result<Token, TcsError> {
self.bump();
let mut raw = String::from("\"");
loop {
match self.bump() {
Some('"') => {
raw.push('"');
break;
}
Some('\\') => {
raw.push('\\');
let escaped = self.bump().ok_or_else(|| {
TcsError::new("TCS-E0002", "unterminated string escape").at(line, column)
})?;
raw.push(escaped);
}
Some('\n' | '\r') | None => {
return Err(TcsError::new("TCS-E0002", "unterminated string").at(line, column));
}
Some(character) => raw.push(character),
}
}
let decoded: String = serde_json::from_str(&raw).map_err(|error| {
TcsError::new("TCS-E0002", format!("invalid string escape: {error}")).at(line, column)
})?;
Ok(Token {
kind: TokenKind::Text(decoded),
line,
column,
})
}
}
struct Parser<'a> {
lexer: Lexer<'a>,
current: Token,
}
impl<'a> Parser<'a> {
fn new(source: &'a str) -> Result<Self, TcsError> {
let mut lexer = Lexer::new(source)?;
let current = lexer.token()?;
Ok(Self { lexer, current })
}
fn advance(&mut self) -> Result<Token, TcsError> {
let old = self.current.clone();
self.current = self.lexer.token()?;
Ok(old)
}
fn symbol(&mut self, expected: TokenKind) -> Result<(), TcsError> {
if std::mem::discriminant(&self.current.kind) == std::mem::discriminant(&expected) {
self.advance()?;
Ok(())
} else {
Err(TcsError::new(
"TCS-E1001",
format!("expected {expected:?}, found {:?}", self.current.kind),
)
.at(self.current.line, self.current.column))
}
}
fn identifier(&mut self) -> Result<String, TcsError> {
match self.advance()? {
Token {
kind: TokenKind::Identifier(value),
..
} => Ok(value),
token => Err(TcsError::new(
"TCS-E1001",
format!("expected identifier, found {:?}", token.kind),
)
.at(token.line, token.column)),
}
}
fn document(&mut self) -> Result<TcsDocument, TcsError> {
let prologue = self.identifier()?;
if prologue != "TCS" {
return Err(TcsError::new("TCS-E1001", "file must begin with TCS"));
}
let major = match self.advance()?.kind {
TokenKind::Int(value) if value >= 0 => value,
_ => return Err(TcsError::new("TCS-E1001", "invalid language version")),
};
self.symbol(TokenKind::Dot)?;
let minor = match self.advance()?.kind {
TokenKind::Int(value) if value >= 0 => value,
_ => return Err(TcsError::new("TCS-E1001", "invalid language version")),
};
self.symbol(TokenKind::Semicolon)?;
let declaration_kind = self.identifier()?;
if !matches!(
declaration_kind.as_str(),
"PROGRAM" | "MODULE" | "PROTOCOL" | "COMPILER" | "EVENT" | "RECEIPT"
) {
return Err(TcsError::new(
"TCS-E1001",
format!("unknown declaration kind {declaration_kind}"),
));
}
let declaration_id = self.identifier()?;
let body = self.block()?;
if self.current.kind != TokenKind::Eof {
return Err(
TcsError::new("TCS-E1001", "trailing tokens after declaration")
.at(self.current.line, self.current.column),
);
}
Ok(TcsDocument {
language_version: format!("{major}.{minor}"),
declaration_kind,
declaration_id,
body,
})
}
fn block(&mut self) -> Result<BTreeMap<String, TcsValue>, TcsError> {
self.symbol(TokenKind::LBrace)?;
let mut object = BTreeMap::new();
while self.current.kind != TokenKind::RBrace {
if self.current.kind == TokenKind::Eof {
return Err(TcsError::new("TCS-E1001", "unterminated block"));
}
let line = self.current.line;
let column = self.current.column;
let key = self.identifier()?;
let value = match self.current.kind {
TokenKind::LBrace => TcsValue::Object(self.block()?),
TokenKind::Colon => {
self.advance()?;
self.type_expression()?;
self.symbol(TokenKind::Equal)?;
let value = self.value()?;
self.symbol(TokenKind::Semicolon)?;
value
}
TokenKind::Equal => {
self.advance()?;
let value = self.value()?;
self.symbol(TokenKind::Semicolon)?;
value
}
_ => {
return Err(TcsError::new(
"TCS-E1001",
"expected block, type annotation, or assignment",
)
.at(self.current.line, self.current.column));
}
};
if object.insert(key.clone(), value).is_some() {
return Err(TcsError::new(
"TCS-E1002",
format!("duplicate field or section {key}"),
)
.at(line, column));
}
}
self.advance()?;
Ok(object)
}
fn type_expression(&mut self) -> Result<(), TcsError> {
self.identifier()?;
if self.current.kind == TokenKind::Less {
self.advance()?;
self.type_expression()?;
while self.current.kind == TokenKind::Comma {
self.advance()?;
self.type_expression()?;
}
self.symbol(TokenKind::Greater)?;
}
Ok(())
}
fn value(&mut self) -> Result<TcsValue, TcsError> {
match self.advance()? {
Token {
kind: TokenKind::Text(value),
..
}
| Token {
kind: TokenKind::Identifier(value),
..
} => Ok(TcsValue::Text(value)),
Token {
kind: TokenKind::Int(value),
..
} => Ok(TcsValue::Int(value)),
Token {
kind: TokenKind::Bool(value),
..
} => Ok(TcsValue::Bool(value)),
Token {
kind: TokenKind::Null,
..
} => Ok(TcsValue::Null),
Token {
kind: TokenKind::LBracket,
..
} => self.array(),
Token {
kind: TokenKind::LBrace,
..
} => self.inline_object(),
token => Err(TcsError::new(
"TCS-E1001",
format!("invalid value token {:?}", token.kind),
)
.at(token.line, token.column)),
}
}
fn array(&mut self) -> Result<TcsValue, TcsError> {
let mut values = Vec::new();
if self.current.kind != TokenKind::RBracket {
loop {
values.push(self.value()?);
if self.current.kind != TokenKind::Comma {
break;
}
self.advance()?;
}
}
self.symbol(TokenKind::RBracket)?;
Ok(TcsValue::List(values))
}
fn inline_object(&mut self) -> Result<TcsValue, TcsError> {
let mut object = BTreeMap::new();
if self.current.kind != TokenKind::RBrace {
loop {
let key = self.identifier()?;
self.symbol(TokenKind::Colon)?;
let value = self.value()?;
if object.insert(key.clone(), value).is_some() {
return Err(TcsError::new(
"TCS-E1002",
format!("duplicate inline field {key}"),
));
}
if self.current.kind != TokenKind::Comma {
break;
}
self.advance()?;
}
}
self.symbol(TokenKind::RBrace)?;
Ok(TcsValue::Object(object))
}
}
pub fn parse(source: &str) -> Result<TcsDocument, TcsError> {
Parser::new(source)?.document()
}
const PROGRAM_SECTIONS: [&str; 15] = [
"header",
"source",
"subject",
"target",
"inputs",
"outputs",
"conditions",
"actions",
"authority",
"resources",
"failure",
"stop",
"cleanup",
"rollback",
"receipt",
];
fn required_text<'a>(
object: &'a BTreeMap<String, TcsValue>,
field: &str,
) -> Result<&'a str, TcsError> {
object
.get(field)
.ok_or_else(|| TcsError::new("TCS-E1004", format!("required field {field} missing")))?
.text(field)
}
pub fn validate_program(document: &TcsDocument) -> Result<(), TcsError> {
if document.language_version != "0.1" {
return Err(TcsError::new(
"TCS-E2102",
format!("unsupported TCS version {}", document.language_version),
));
}
if document.declaration_kind != "PROGRAM" {
return Err(TcsError::new(
"TCS-E2001",
"Stage-0 executable subset accepts PROGRAM only",
));
}
for section in PROGRAM_SECTIONS {
document.body.get(section).ok_or_else(|| {
TcsError::new("TCS-E1004", format!("required section {section} missing"))
})?;
}
for section in document.body.keys() {
if !PROGRAM_SECTIONS.contains(&section.as_str()) {
return Err(TcsError::new(
"TCS-E1003",
format!("unknown PROGRAM section {section}"),
));
}
}
let header = document.body["header"].object("header")?;
for field in [
"schema",
"name_zh",
"name_en",
"version",
"language",
"profile",
"lifecycle",
"canonical_uri",
] {
required_text(header, field)?;
}
if !header.contains_key("protocols") || !header.contains_key("compatibility") {
return Err(TcsError::new(
"TCS-E1004",
"header requires protocols and compatibility",
));
}
let actions = document.body["actions"].object("actions")?;
if actions.is_empty() {
return Err(TcsError::new("TCS-E1004", "actions must not be empty"));
}
for (action_id, action) in actions {
let action = action.object(action_id)?;
let operation = required_text(action, "operation")?;
if operation != "CORE.ECHO" {
return Err(TcsError::new(
"TCS-E2101",
format!("unregistered Stage-0 operation {operation}"),
));
}
for field in ["input_refs", "output_refs", "on_success", "on_failure"] {
if !action.contains_key(field) {
return Err(TcsError::new(
"TCS-E1004",
format!("action {action_id} missing {field}"),
));
}
}
}
Ok(())
}
pub fn sha256_hex(bytes: impl AsRef<[u8]>) -> String {
format!("{:x}", Sha256::digest(bytes.as_ref()))
}
pub fn compile(source: &str) -> Result<JsonValue, TcsError> {
let document = parse(source)?;
validate_program(&document)?;
let source_sha256 = sha256_hex(source.as_bytes());
let body = serde_json::to_value(&document.body)
.map_err(|error| TcsError::new("TCS-E8001", error.to_string()))?;
Ok(json!({
"schema": "guanghu.gir/v1",
"identity": {
"gir_id": format!("GIR-{}", document.declaration_id),
"program_id": document.declaration_id,
"language_version": document.language_version,
},
"compiled_from": {
"source_sha256": source_sha256,
"compiler_id": COMPILER_ID,
"compiler_state": COMPILER_STATE,
},
"subject": body["subject"].clone(),
"exact_target": body["target"].clone(),
"inputs": body["inputs"].clone(),
"outputs": body["outputs"].clone(),
"conditions": body["conditions"].clone(),
"deterministic_action_graph": body["actions"].clone(),
"authority_proof": body["authority"].clone(),
"resource_plan": body["resources"].clone(),
"failure_plan": body["failure"].clone(),
"timeout_and_stop": body["stop"].clone(),
"cleanup_plan": body["cleanup"].clone(),
"rollback_plan": body["rollback"].clone(),
"receipt_plan": body["receipt"].clone(),
"unresolved_natural_language": false,
"native_self_hosted": false,
}))
}
pub fn canonical_json(value: &JsonValue) -> Result<String, TcsError> {
serde_json::to_string_pretty(value)
.map(|mut text| {
text.push('\n');
text
})
.map_err(|error| TcsError::new("TCS-E8001", error.to_string()))
}
fn safe_relative_path(value: &str) -> Result<PathBuf, TcsError> {
let path = Path::new(value);
if path.is_absolute() {
return Err(TcsError::new("TCS-E4002", "absolute target path forbidden"));
}
let mut result = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::Normal(value) => result.push(value),
_ => {
return Err(TcsError::new(
"TCS-E4002",
"target path traversal forbidden",
));
}
}
}
if result.as_os_str().is_empty() {
return Err(TcsError::new("TCS-E4002", "empty target path forbidden"));
}
Ok(result)
}
fn json_text<'a>(value: &'a JsonValue, pointer: &str) -> Result<&'a str, TcsError> {
value
.pointer(pointer)
.and_then(JsonValue::as_str)
.ok_or_else(|| TcsError::new("TCS-E2001", format!("missing text at {pointer}")))
}
pub fn run_echo_gir(gir: &JsonValue, root: &Path) -> Result<PathBuf, TcsError> {
if gir.get("schema").and_then(JsonValue::as_str) != Some("guanghu.gir/v1") {
return Err(TcsError::new("TCS-E2001", "unsupported GIR schema"));
}
if gir
.pointer("/compiled_from/compiler_state")
.and_then(JsonValue::as_str)
!= Some(COMPILER_STATE)
{
return Err(TcsError::new("TCS-E3002", "unknown compiler provenance"));
}
let actions = gir
.get("deterministic_action_graph")
.and_then(JsonValue::as_object)
.ok_or_else(|| TcsError::new("TCS-E2001", "missing action graph"))?;
if actions.len() != 1 {
return Err(TcsError::new(
"TCS-E2101",
"Stage-0 runner requires exactly one action",
));
}
let action = actions.values().next().expect("one action");
if action.get("operation").and_then(JsonValue::as_str) != Some("CORE.ECHO") {
return Err(TcsError::new("TCS-E2101", "runner permits CORE.ECHO only"));
}
let message = json_text(gir, "/inputs/MESSAGE/value")?;
let target = json_text(gir, "/exact_target/exact_path")?;
let receipt_target = json_text(gir, "/receipt_plan/machine_path")?;
if target != receipt_target {
return Err(TcsError::new(
"TCS-E6001",
"target and receipt path must match",
));
}
let relative = safe_relative_path(target)?;
let output = root.join(relative);
let parent = output
.parent()
.ok_or_else(|| TcsError::new("TCS-E4002", "target has no parent"))?;
fs::create_dir_all(parent).map_err(|error| {
TcsError::new(
"TCS-E8001",
format!("cannot create receipt parent: {error}"),
)
})?;
let receipt = json!({
"schema": "tcs.execution-receipt/v1",
"state": "EXECUTED_TARGET_READBACK_VERIFIED",
"program_id": json_text(gir, "/identity/program_id")?,
"operation": "CORE.ECHO",
"output": { "ECHOED": message },
"gir_sha256": sha256_hex(canonical_json(gir)?.as_bytes()),
"compiler_state": COMPILER_STATE,
"native_self_hosted": false,
});
let encoded = canonical_json(&receipt)?;
fs::write(&output, encoded.as_bytes())
.map_err(|error| TcsError::new("TCS-E8001", format!("cannot write receipt: {error}")))?;
let readback = fs::read(&output)
.map_err(|error| TcsError::new("TCS-E6002", format!("cannot read receipt: {error}")))?;
if readback != encoded.as_bytes() {
return Err(TcsError::new("TCS-E6002", "receipt readback differs"));
}
Ok(output)
}
pub fn compile_file(source: &Path, output: &Path) -> Result<(), TcsError> {
let source_text = fs::read_to_string(source)
.map_err(|error| TcsError::new("TCS-E3002", format!("cannot read source: {error}")))?;
let gir = compile(&source_text)?;
let encoded = canonical_json(&gir)?;
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| {
TcsError::new("TCS-E8001", format!("cannot create output: {error}"))
})?;
}
fs::write(output, encoded)
.map_err(|error| TcsError::new("TCS-E8001", format!("cannot write GIR: {error}")))
}
pub fn run_gir_file(gir_path: &Path, root: &Path) -> Result<PathBuf, TcsError> {
let text = fs::read_to_string(gir_path)
.map_err(|error| TcsError::new("TCS-E3002", format!("cannot read GIR: {error}")))?;
let gir: JsonValue = serde_json::from_str(&text)
.map_err(|error| TcsError::new("TCS-E2001", format!("invalid GIR JSON: {error}")))?;
run_echo_gir(&gir, root)
}

View file

@ -0,0 +1,35 @@
use std::{env, fs, path::Path};
fn usage() -> &'static str {
"usage: tcs-stage0 parse <source.tcs> | compile <source.tcs> <output.gir.json> | run <input.gir.json> <allowed-root>"
}
fn main() {
if let Err(error) = run() {
eprintln!("{error}");
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let arguments: Vec<String> = env::args().skip(1).collect();
match arguments.as_slice() {
[command, source] if command == "parse" => {
let text = fs::read_to_string(source)?;
let document = tcs_stage0::parse(&text)?;
println!("{}", serde_json::to_string_pretty(&document)?);
Ok(())
}
[command, source, output] if command == "compile" => {
tcs_stage0::compile_file(Path::new(source), Path::new(output))?;
println!("COMPILED {source} -> {output}");
Ok(())
}
[command, gir, root] if command == "run" => {
let receipt = tcs_stage0::run_gir_file(Path::new(gir), Path::new(root))?;
println!("EXECUTED receipt={}", receipt.display());
Ok(())
}
_ => Err(usage().into()),
}
}

View file

@ -0,0 +1,76 @@
use serde_json::Value as JsonValue;
use std::{fs, path::Path};
fn example() -> String {
fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../language/examples/ECHO-MODULE.tcs"),
)
.expect("example source")
}
#[test]
fn parses_and_compiles_the_registered_echo_program() {
let source = example();
let document = tcs_stage0::parse(&source).expect("parse");
assert_eq!(document.language_version, "0.1");
assert_eq!(document.declaration_kind, "PROGRAM");
assert_eq!(document.declaration_id, "HLP-PROGRAM-ECHO-0001");
let gir = tcs_stage0::compile(&source).expect("compile");
assert_eq!(gir["schema"], "guanghu.gir/v1");
assert_eq!(gir["native_self_hosted"], false);
assert_eq!(
gir.pointer("/deterministic_action_graph/A1/operation")
.and_then(JsonValue::as_str),
Some("CORE.ECHO")
);
}
#[test]
fn rejects_duplicate_fields_and_missing_sections() {
let duplicate = "TCS 0.1; EVENT E1 { x = 1; x = 2; }";
let error = tcs_stage0::parse(duplicate).expect_err("duplicate must fail");
assert_eq!(error.code, "TCS-E1002");
let missing = "TCS 0.1; PROGRAM P1 { header { schema = \"tcs.program/v1\"; name_zh = \"x\"; name_en = \"x\"; version = \"0.1.0\"; language = \"TCS/0.1\"; profile = \"HLDP-NP/1\"; protocols = []; lifecycle = \"CANDIDATE\"; canonical_uri = \"x\"; compatibility = []; } }";
let document = tcs_stage0::parse(missing).expect("syntax is valid");
let error = tcs_stage0::validate_program(&document).expect_err("missing sections must fail");
assert_eq!(error.code, "TCS-E1004");
}
#[test]
fn rejects_unregistered_operations() {
let source = example().replace("CORE.ECHO", "ABSORB");
let error = tcs_stage0::compile(&source).expect_err("ABSORB must fail closed");
assert_eq!(error.code, "TCS-E2101");
}
#[test]
fn executes_gir_inside_exact_root_and_reads_receipt_back() {
let root = tempfile::tempdir().expect("temp root");
let gir = tcs_stage0::compile(&example()).expect("compile");
let receipt = tcs_stage0::run_echo_gir(&gir, root.path()).expect("run");
assert!(receipt.starts_with(root.path()));
let value: JsonValue =
serde_json::from_str(&fs::read_to_string(receipt).expect("receipt")).expect("receipt json");
assert_eq!(value["state"], "EXECUTED_TARGET_READBACK_VERIFIED");
assert_eq!(
value.pointer("/output/ECHOED").and_then(JsonValue::as_str),
Some("光湖语言已经进入机器执行链")
);
assert_eq!(value["native_self_hosted"], false);
}
#[test]
fn rejects_path_traversal_and_target_receipt_mismatch() {
let root = tempfile::tempdir().expect("temp root");
let mut gir = tcs_stage0::compile(&example()).expect("compile");
gir["exact_target"]["exact_path"] = JsonValue::String("../escape".to_owned());
gir["receipt_plan"]["machine_path"] = JsonValue::String("../escape".to_owned());
let error = tcs_stage0::run_echo_gir(&gir, root.path()).expect_err("traversal must fail");
assert_eq!(error.code, "TCS-E4002");
let mut mismatch = tcs_stage0::compile(&example()).expect("compile");
mismatch["receipt_plan"]["machine_path"] = JsonValue::String("./build/other".to_owned());
let error = tcs_stage0::run_echo_gir(&mismatch, root.path()).expect_err("mismatch must fail");
assert_eq!(error.code, "TCS-E6001");
}