define TCS language and prove stage0 execution slice
This commit is contained in:
parent
8cfdba210f
commit
3959ae4270
31 changed files with 2449 additions and 5 deletions
284
bootstrap/tcs-stage0/Cargo.lock
generated
Normal file
284
bootstrap/tcs-stage0/Cargo.lock
generated
Normal 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"
|
||||
15
bootstrap/tcs-stage0/Cargo.toml
Normal file
15
bootstrap/tcs-stage0/Cargo.toml
Normal 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"
|
||||
|
||||
805
bootstrap/tcs-stage0/src/lib.rs
Normal file
805
bootstrap/tcs-stage0/src/lib.rs
Normal 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(§ion.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)
|
||||
}
|
||||
35
bootstrap/tcs-stage0/src/main.rs
Normal file
35
bootstrap/tcs-stage0/src/main.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
76
bootstrap/tcs-stage0/tests/stage0.rs
Normal file
76
bootstrap/tcs-stage0/tests/stage0.rs
Normal 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");
|
||||
}
|
||||
Loading…
Reference in a new issue