make TCS compiler GIR control validation and lowering

This commit is contained in:
冰朔 2026-08-21 17:25:09 +08:00
commit 6e89b82d88
5 changed files with 271 additions and 17 deletions

View file

@ -142,3 +142,24 @@ fn compiler_gir_tampering_and_negative_program_fail_closed() {
.expect_err("unregistered operation must still fail through compiler GIR");
assert_eq!(error.code, "TCS-E2101");
}
#[test]
fn tcs_compiler_source_controls_operation_policy_and_gir_lowering() {
let compiler_source = compiler_source()
.replace(
"registered_operations = [\"CORE.ECHO\"]",
"registered_operations = [\"ABSORB\"]",
)
.replace("to = \"exact_target\"", "to = \"policy_selected_target\"");
let compiler = tcs_stage0::bootstrap_compiler(&compiler_source).expect("policy compiler GIR");
let absorb_program = example().replace("CORE.ECHO", "ABSORB");
let gir = tcs_stage0::compile_with_compiler_gir(&compiler, &absorb_program)
.expect("TCS source policy permits ABSORB at compile time");
assert!(gir.get("policy_selected_target").is_some());
assert!(gir.get("exact_target").is_none());
let error = tcs_stage0::compile_with_compiler_gir(&compiler, &example())
.expect_err("TCS source policy rejects operation removed from its registry");
assert_eq!(error.code, "TCS-E2101");
}

View file

@ -2,7 +2,7 @@
"compiled_from": {
"compiler_id": "TCS-STAGE0-IGNITION-0001",
"compiler_state": "FOREIGN_HOST_BOOTSTRAP_SEED_NOT_SELF_HOSTED",
"source_sha256": "d375adb7a715c93320c14f3e38a3fa10bc764493d62cfa671e5dfd2d4d3b049f"
"source_sha256": "49259d15c57f3a725a7507ae746c372cebf9b3fc58dba580f7c41456d2344f73"
},
"compiler_definition": {
"canonicalization": {
@ -116,6 +116,26 @@
"from": "all",
"to": "unresolved_natural_language",
"transform": "MUST_BE_FALSE"
},
"G013": {
"from": "inputs",
"to": "inputs",
"transform": "COPY_RESOLVED_VALUES"
},
"G014": {
"from": "outputs",
"to": "outputs",
"transform": "COPY_RESOLVED_VALUES"
},
"G015": {
"from": "conditions",
"to": "conditions",
"transform": "COPY_RESOLVED_CONDITIONS"
},
"G016": {
"from": "failure",
"to": "failure_plan",
"transform": "COPY_EXPLICIT_FAILURE_SEMANTICS"
}
},
"output_contract": {
@ -237,7 +257,27 @@
"V010": {
"error": "TCS-E4001",
"rule": "RELATION_AND_EMOTION_DO_NOT_GRANT_AUTHORITY"
}
},
"registered_operations": [
"CORE.ECHO"
],
"required_sections": [
"header",
"source",
"subject",
"target",
"inputs",
"outputs",
"conditions",
"actions",
"authority",
"resources",
"failure",
"stop",
"cleanup",
"rollback",
"receipt"
]
},
"self_host": {
"compiler_A_compiles_stage1_to": "compiler-B.gir",
@ -329,7 +369,7 @@
"execution_abi": "TCS-COMPILER-GIR-EXEC/1",
"identity": {
"compiler_id": "TCS-COMPILER-STAGE1-0001",
"definition_sha256": "c2bf0e4e93f9952daf661628aa7b17891718c53b9fbb5e77636f5f40812ff9f6",
"definition_sha256": "3e27046ef79f33c1f60705d6774773265df30e2299c390673ae9b8029c8235b2",
"language_version": "0.1"
},
"native_self_hosted": false,

View file

@ -2,7 +2,7 @@
"compiled_from": {
"compiler_id": "TCS-COMPILER-STAGE1-0001",
"compiler_state": "TCS_COMPILER_GIR_EXECUTED",
"source_sha256": "d375adb7a715c93320c14f3e38a3fa10bc764493d62cfa671e5dfd2d4d3b049f"
"source_sha256": "49259d15c57f3a725a7507ae746c372cebf9b3fc58dba580f7c41456d2344f73"
},
"compiler_definition": {
"canonicalization": {
@ -116,6 +116,26 @@
"from": "all",
"to": "unresolved_natural_language",
"transform": "MUST_BE_FALSE"
},
"G013": {
"from": "inputs",
"to": "inputs",
"transform": "COPY_RESOLVED_VALUES"
},
"G014": {
"from": "outputs",
"to": "outputs",
"transform": "COPY_RESOLVED_VALUES"
},
"G015": {
"from": "conditions",
"to": "conditions",
"transform": "COPY_RESOLVED_CONDITIONS"
},
"G016": {
"from": "failure",
"to": "failure_plan",
"transform": "COPY_EXPLICIT_FAILURE_SEMANTICS"
}
},
"output_contract": {
@ -237,7 +257,27 @@
"V010": {
"error": "TCS-E4001",
"rule": "RELATION_AND_EMOTION_DO_NOT_GRANT_AUTHORITY"
}
},
"registered_operations": [
"CORE.ECHO"
],
"required_sections": [
"header",
"source",
"subject",
"target",
"inputs",
"outputs",
"conditions",
"actions",
"authority",
"resources",
"failure",
"stop",
"cleanup",
"rollback",
"receipt"
]
},
"self_host": {
"compiler_A_compiles_stage1_to": "compiler-B.gir",
@ -329,7 +369,7 @@
"execution_abi": "TCS-COMPILER-GIR-EXEC/1",
"identity": {
"compiler_id": "TCS-COMPILER-STAGE1-0001",
"definition_sha256": "c2bf0e4e93f9952daf661628aa7b17891718c53b9fbb5e77636f5f40812ff9f6",
"definition_sha256": "3e27046ef79f33c1f60705d6774773265df30e2299c390673ae9b8029c8235b2",
"language_version": "0.1"
},
"native_self_hosted": true,

View file

@ -45,6 +45,8 @@ COMPILER TCS-COMPILER-STAGE1-0001 {
floating_point = "FORBIDDEN_V0_1";
}
program_validation {
required_sections = ["header", "source", "subject", "target", "inputs", "outputs", "conditions", "actions", "authority", "resources", "failure", "stop", "cleanup", "rollback", "receipt"];
registered_operations = ["CORE.ECHO"];
V001 { rule = "EXACT_REQUIRED_SECTIONS"; standard = "TCS-FIELD-STANDARD-0001"; error_missing = "TCS-E1004"; error_unknown = "TCS-E1003"; }
V002 { rule = "CLOSED_FIELDS_PER_SECTION"; error = "TCS-E1003"; }
V003 { rule = "ALL_INPUT_OUTPUT_AND_ACTION_REFS_RESOLVE"; error = "TCS-E2002"; }
@ -69,6 +71,10 @@ COMPILER TCS-COMPILER-STAGE1-0001 {
G010 { from = "rollback"; to = "rollback_plan"; transform = "COPY_ACTIONS_AND_VERIFICATION"; }
G011 { from = "receipt"; to = "receipt_plan"; transform = "BIND_MACHINE_AND_HUMAN_OUTPUTS"; }
G012 { from = "all"; to = "unresolved_natural_language"; transform = "MUST_BE_FALSE"; }
G013 { from = "inputs"; to = "inputs"; transform = "COPY_RESOLVED_VALUES"; }
G014 { from = "outputs"; to = "outputs"; transform = "COPY_RESOLVED_VALUES"; }
G015 { from = "conditions"; to = "conditions"; transform = "COPY_RESOLVED_CONDITIONS"; }
G016 { from = "failure"; to = "failure_plan"; transform = "COPY_EXPLICIT_FAILURE_SEMANTICS"; }
}
canonicalization {
object_keys = "UTF8_BYTE_ORDER";

View file

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use sha2::{Digest, Sha256};
use std::{
collections::BTreeMap,
collections::{BTreeMap, BTreeSet},
fs,
path::{Component, Path, PathBuf},
};
@ -608,13 +608,26 @@ pub fn validate_program(document: &TcsDocument) -> Result<(), TcsError> {
"Stage-0 executable subset accepts PROGRAM only",
));
}
for section in PROGRAM_SECTIONS {
let sections = PROGRAM_SECTIONS
.iter()
.map(|value| (*value).to_owned())
.collect::<Vec<_>>();
let operations = BTreeSet::from(["CORE.ECHO".to_owned()]);
validate_program_contract(document, &sections, &operations)
}
fn validate_program_contract(
document: &TcsDocument,
sections: &[String],
registered_operations: &BTreeSet<String>,
) -> Result<(), TcsError> {
for section in 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()) {
if !sections.contains(section) {
return Err(TcsError::new(
"TCS-E1003",
format!("unknown PROGRAM section {section}"),
@ -647,7 +660,7 @@ pub fn validate_program(document: &TcsDocument) -> Result<(), TcsError> {
for (action_id, action) in actions {
let action = action.object(action_id)?;
let operation = required_text(action, "operation")?;
if operation != "CORE.ECHO" {
if !registered_operations.contains(operation) {
return Err(TcsError::new(
"TCS-E2101",
format!("unregistered TCS operation {operation}"),
@ -665,6 +678,99 @@ pub fn validate_program(document: &TcsDocument) -> Result<(), TcsError> {
Ok(())
}
#[derive(Debug)]
struct CompilerPolicy {
required_sections: Vec<String>,
registered_operations: BTreeSet<String>,
lowerings: BTreeMap<String, String>,
output_schema: String,
}
fn compiler_policy(compiler: &JsonValue) -> Result<CompilerPolicy, TcsError> {
fn text_array(value: &JsonValue, pointer: &str) -> Result<Vec<String>, TcsError> {
value
.pointer(pointer)
.and_then(JsonValue::as_array)
.ok_or_else(|| TcsError::new("TCS-E3002", format!("missing array {pointer}")))?
.iter()
.map(|item| {
item.as_str().map(str::to_owned).ok_or_else(|| {
TcsError::new("TCS-E3002", format!("non-text item in {pointer}"))
})
})
.collect()
}
let required_sections = text_array(
compiler,
"/compiler_definition/program_validation/required_sections",
)?;
let registered_operations = text_array(
compiler,
"/compiler_definition/program_validation/registered_operations",
)?
.into_iter()
.collect::<BTreeSet<_>>();
if required_sections.is_empty() || registered_operations.is_empty() {
return Err(TcsError::new(
"TCS-E3002",
"compiler policy must not be empty",
));
}
let lowering_rules = compiler
.pointer("/compiler_definition/lowering_to_gir")
.and_then(JsonValue::as_object)
.ok_or_else(|| TcsError::new("TCS-E3002", "lowering rules missing"))?;
let mut lowerings = BTreeMap::new();
for rule in lowering_rules.values() {
let Some(from) = rule.get("from").and_then(JsonValue::as_str) else {
continue;
};
let to = rule
.get("to")
.and_then(JsonValue::as_str)
.ok_or_else(|| TcsError::new("TCS-E3002", "lowering target missing"))?;
if document_section(from) && lowerings.insert(from.to_owned(), to.to_owned()).is_some() {
return Err(TcsError::new("TCS-E3002", "duplicate lowering source"));
}
}
let required_lowerings = [
"subject",
"target",
"inputs",
"outputs",
"conditions",
"actions",
"authority",
"resources",
"failure",
"stop",
"cleanup",
"rollback",
"receipt",
];
if required_lowerings
.iter()
.any(|field| !lowerings.contains_key(*field))
{
return Err(TcsError::new(
"TCS-E3002",
"compiler lowering map is incomplete",
));
}
Ok(CompilerPolicy {
required_sections,
registered_operations,
lowerings,
output_schema: json_text(compiler, "/compiler_definition/output_contract/schema")?
.to_owned(),
})
}
fn document_section(value: &str) -> bool {
!matches!(value, "header" | "source" | "all")
}
fn validate_exact_sections(document: &TcsDocument, expected: &[&str]) -> Result<(), TcsError> {
for section in expected {
document.body.get(*section).ok_or_else(|| {
@ -759,6 +865,7 @@ pub fn sha256_hex(bytes: impl AsRef<[u8]>) -> String {
format!("{:x}", Sha256::digest(bytes.as_ref()))
}
#[cfg(feature = "bootstrap")]
fn lower_program(
document: &TcsDocument,
source_sha256: &str,
@ -798,6 +905,44 @@ fn lower_program(
}))
}
fn lower_program_with_policy(
document: &TcsDocument,
source_sha256: &str,
compiler_id: &str,
policy: &CompilerPolicy,
) -> Result<JsonValue, TcsError> {
let mut gir = json!({
"schema": policy.output_schema,
"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": "TCS_COMPILER_GIR_EXECUTED",
},
"unresolved_natural_language": false,
"native_self_hosted": true,
});
let object = gir
.as_object_mut()
.ok_or_else(|| TcsError::new("TCS-E8001", "GIR root must be object"))?;
for (from, to) in &policy.lowerings {
let value = document
.body
.get(from)
.ok_or_else(|| TcsError::new("TCS-E3002", format!("lowering source {from} missing")))?;
object.insert(
to.clone(),
serde_json::to_value(value)
.map_err(|error| TcsError::new("TCS-E8001", error.to_string()))?,
);
}
Ok(gir)
}
fn compiler_definition_sha256(document: &TcsDocument) -> Result<String, TcsError> {
let definition = serde_json::to_vec(&document.body)
.map_err(|error| TcsError::new("TCS-E8001", error.to_string()))?;
@ -922,18 +1067,20 @@ pub fn compile_with_compiler_gir(
source: &str,
) -> Result<JsonValue, TcsError> {
let (compiler_id, _) = verify_compiler_gir(compiler)?;
let policy = compiler_policy(compiler)?;
let document = parse(source)?;
let source_sha256 = sha256_hex(source.as_bytes());
match document.declaration_kind.as_str() {
"PROGRAM" => {
validate_program(&document)?;
lower_program(
if document.language_version != "0.1" {
return Err(TcsError::new("TCS-E2102", "unsupported TCS version"));
}
validate_program_contract(
&document,
&source_sha256,
compiler_id,
"TCS_COMPILER_GIR_EXECUTED",
true,
)
&policy.required_sections,
&policy.registered_operations,
)?;
lower_program_with_policy(&document, &source_sha256, compiler_id, &policy)
}
"COMPILER" => {
validate_compiler(&document)?;