self-host TCS compiler GIR and add portable host runtime

This commit is contained in:
冰朔 2026-08-21 17:15:23 +08:00
commit ff29e52a32
33 changed files with 3848 additions and 831 deletions

View file

@ -222,12 +222,20 @@ dependencies = [
]
[[package]]
name = "tcs-stage0"
name = "tcs-gir-runtime"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"sha2",
]
[[package]]
name = "tcs-stage0"
version = "0.1.0"
dependencies = [
"serde_json",
"tcs-gir-runtime",
"tempfile",
]

View file

@ -6,10 +6,8 @@ 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"
tcs-gir-runtime = { path = "../../native-runtime/tcs-gir-runtime", features = ["bootstrap"] }
[dev-dependencies]
tempfile = "3"

View file

@ -1,805 +1,5 @@
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use sha2::{Digest, Sha256};
use std::{
collections::BTreeMap,
fs,
path::{Component, Path, PathBuf},
};
//! Replaceable ignition shell. Language parsing and compiler-GIR execution live
//! in the HoloLake native runtime crate so Stage-1 operation does not depend on
//! this Stage-0 package remaining enabled.
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)
}
pub use tcs_gir_runtime::*;

View file

@ -1,7 +1,7 @@
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>"
"usage: tcs-stage0 parse <source.tcs> | compile <source.tcs> <output.gir.json> | bootstrap-compiler <compiler.tcs> <compiler.gir.json> | compile-with <compiler.gir.json> <source.tcs> <output.gir.json> | run <input.gir.json> <allowed-root>"
}
fn main() {
@ -25,6 +25,21 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
println!("COMPILED {source} -> {output}");
Ok(())
}
[command, source, output] if command == "bootstrap-compiler" => {
let source_text = fs::read_to_string(source)?;
let compiler = tcs_stage0::bootstrap_compiler(&source_text)?;
fs::write(output, tcs_stage0::canonical_json(&compiler)?)?;
println!("BOOTSTRAPPED {source} -> {output}");
Ok(())
}
[command, compiler, source, output] if command == "compile-with" => {
let compiler: serde_json::Value = serde_json::from_str(&fs::read_to_string(compiler)?)?;
let source_text = fs::read_to_string(source)?;
let gir = tcs_stage0::compile_with_compiler_gir(&compiler, &source_text)?;
fs::write(output, tcs_stage0::canonical_json(&gir)?)?;
println!("TCS_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());

View file

@ -8,6 +8,21 @@ fn example() -> String {
.expect("example source")
}
fn compiler_source() -> String {
fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../language/compiler/TCS-COMPILER-STAGE1.tcs"),
)
.expect("compiler source")
}
fn module_source() -> String {
fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../language/examples/ECHO-PACKAGE.tcs"),
)
.expect("module source")
}
#[test]
fn parses_and_compiles_the_registered_echo_program() {
let source = example();
@ -74,3 +89,56 @@ fn rejects_path_traversal_and_target_receipt_mismatch() {
let error = tcs_stage0::run_echo_gir(&mismatch, root.path()).expect_err("mismatch must fail");
assert_eq!(error.code, "TCS-E6001");
}
#[test]
fn tcs_compiler_reaches_semantic_fixed_point_and_compiles_without_stage0() {
let source = compiler_source();
let compiler_a = tcs_stage0::bootstrap_compiler(&source).expect("Stage-0 creates compiler A");
assert_eq!(compiler_a["schema"], "guanghu.compiler-gir/v1");
assert_eq!(compiler_a["native_self_hosted"], false);
let compiler_b = tcs_stage0::compile_with_compiler_gir(&compiler_a, &source)
.expect("compiler A creates compiler B");
assert_eq!(compiler_b["native_self_hosted"], true);
assert_eq!(
compiler_a.pointer("/identity/definition_sha256"),
compiler_b.pointer("/identity/definition_sha256")
);
let program = tcs_stage0::compile_with_compiler_gir(&compiler_b, &example())
.expect("compiler B compiles program without Stage-0 compile path");
assert_eq!(program["native_self_hosted"], true);
assert_eq!(
program
.pointer("/compiled_from/compiler_state")
.and_then(JsonValue::as_str),
Some("TCS_COMPILER_GIR_EXECUTED")
);
let module = tcs_stage0::compile_with_compiler_gir(&compiler_b, &module_source())
.expect("compiler B compiles module");
assert_eq!(module["schema"], "guanghu.module-gir/v1");
assert_eq!(
module
.pointer("/manifest/display_name_zh")
.and_then(JsonValue::as_str),
Some("光湖最小回声模块")
);
}
#[test]
fn compiler_gir_tampering_and_negative_program_fail_closed() {
let source = compiler_source();
let mut compiler = tcs_stage0::bootstrap_compiler(&source).expect("compiler A");
compiler["compiler_definition"]["output_contract"]["schema"] =
JsonValue::String("tampered".to_owned());
let error = tcs_stage0::compile_with_compiler_gir(&compiler, &example())
.expect_err("tampered compiler must fail");
assert_eq!(error.code, "TCS-E3002");
let compiler = tcs_stage0::bootstrap_compiler(&source).expect("compiler A");
let negative = example().replace("CORE.ECHO", "ABSORB");
let error = tcs_stage0::compile_with_compiler_gir(&compiler, &negative)
.expect_err("unregistered operation must still fail through compiler GIR");
assert_eq!(error.code, "TCS-E2101");
}