Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions docs/TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -639,10 +639,9 @@ bool f = false;

```javascript
string s1 = "Hello World";
string s2 = 'Single quotes work too';
string escaped = "Line 1\nLine 2\tTabbed";
string quote = "He said \"Hello\"";
string apostrophe = 'It\'s working';
string apostrophe = "It's working";
```

**Hex Literals:**
Expand Down
110 changes: 43 additions & 67 deletions silverscript-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,6 @@ pub enum SilAbiVerificationError {
Version(#[from] ArtifactVersionError),
#[error("contract `{contract}` has state span offset {offset} length {len}, but its compiled script has {script_len} bytes")]
InvalidStateSpan { contract: String, offset: usize, len: usize, script_len: usize },
#[error("contract `{contract}` runtime state field `{field}` has unsupported type `{ty}`")]
UnsupportedRuntimeStateType { contract: String, field: String, ty: String },
#[error("contract `{contract}` state span does not encode its runtime state: {message}")]
InvalidRuntimeStateEncoding { contract: String, message: String },
#[error("contract `{contract}` state span is not canonically encoded")]
Expand Down Expand Up @@ -825,15 +823,6 @@ fn verify_compiled_contract(
script_len: script.len(),
});
};
for field in &contract.runtime_state.fields {
if !is_supported_runtime_state_type(abi, &field.ty) {
return Err(SilAbiVerificationError::UnsupportedRuntimeStateType {
contract: contract_name.to_string(),
field: field.name.clone(),
ty: type_name(&field.ty),
});
}
}
let state_values = decode_runtime_state_script(abi, &contract.runtime_state, state_script).map_err(|err| {
SilAbiVerificationError::InvalidRuntimeStateEncoding { contract: contract_name.to_string(), message: err.to_string() }
})?;
Expand All @@ -854,29 +843,6 @@ fn verify_compiled_contract(
Ok(())
}

fn is_supported_runtime_state_type(abi: &SilAbiArtifact, ty: &TypeArtifact) -> bool {
fn is_supported(abi: &SilAbiArtifact, ty: &TypeArtifact, visiting: &mut BTreeSet<String>) -> bool {
match ty {
TypeArtifact::Struct { name } => {
if !visiting.insert(name.clone()) {
// We don't support cyclic structs.
return false;
}
let supported = abi
.structs
.get(name)
.is_some_and(|structure| structure.fields.iter().all(|field| is_supported(abi, &field.ty, visiting)));
visiting.remove(name);
supported
}
TypeArtifact::FixedArray { item, .. } => is_supported(abi, item, visiting),
_ => fixed_payload_len(ty).is_some(),
}
}

is_supported(abi, ty, &mut BTreeSet::new())
}

fn entry_params(entry: &SilEntryArtifact) -> Vec<(&str, &TypeArtifact)> {
entry.params.iter().map(|param| (param.name.as_str(), &param.ty)).collect()
}
Expand Down Expand Up @@ -1457,7 +1423,7 @@ mod tests {
}

#[test]
fn rejects_noncanonical_and_variable_runtime_state_encodings() {
fn rejects_noncanonical_runtime_state_encodings() {
let mut abi = tiny_sil_abi();
let contract = abi.contracts.get_mut("Foo").unwrap();
contract.runtime_state.fields = vec![RuntimeFieldArtifact { name: "value".to_string(), ty: TypeArtifact::Byte }];
Expand All @@ -1468,47 +1434,57 @@ mod tests {
contract.compiled.template_hash = template_hash(&prefix, &suffix);
contract.compiled.state_span = StateSpanArtifact { offset: prefix.len(), len: state.len() };
assert_eq!(abi.verify(), Err(SilAbiVerificationError::NonCanonicalRuntimeStateEncoding { contract: "Foo".to_string() }));

abi.contracts.get_mut("Foo").unwrap().runtime_state.fields[0].ty = TypeArtifact::Bytes;
assert_eq!(
abi.verify(),
Err(SilAbiVerificationError::UnsupportedRuntimeStateType {
contract: "Foo".to_string(),
field: "value".to_string(),
ty: "bytes".to_string(),
})
);
}

#[test]
fn validates_struct_runtime_state_types_recursively() {
fn accepts_canonical_variable_width_runtime_state_encodings() {
let mut abi = tiny_sil_abi();
abi.structs = BTreeMap::from([
(
"Inner".to_string(),
StructArtifact { fields: vec![FieldArtifact { name: "value".to_string(), ty: TypeArtifact::Byte }] },
),
(
"Outer".to_string(),
StructArtifact {
fields: vec![
FieldArtifact { name: "inner".to_string(), ty: TypeArtifact::Struct { name: "Inner".to_string() } },
FieldArtifact {
name: "values".to_string(),
ty: TypeArtifact::FixedArray { item: Box::new(TypeArtifact::Int), len: 2 },
},
],
let runtime_state = RuntimeStateArtifact {
source: "VariableState".to_string(),
fields: vec![
RuntimeFieldArtifact { name: "text".to_string(), ty: TypeArtifact::Text },
RuntimeFieldArtifact { name: "bytes".to_string(), ty: TypeArtifact::Bytes },
RuntimeFieldArtifact {
name: "numbers".to_string(),
ty: TypeArtifact::DynamicArray { item: Box::new(TypeArtifact::Int) },
},
),
],
};
let values = BTreeMap::from([
("text".to_string(), ArtifactValue::Text("hello".to_string())),
("bytes".to_string(), ArtifactValue::Bytes(vec![1, 2, 3])),
("numbers".to_string(), ArtifactValue::Array(vec![ArtifactValue::Int(7), ArtifactValue::Int(-8)])),
]);
let state = encode_runtime_state_script(&abi, &runtime_state, &values).expect("variable-width state encodes");
let prefix = [0xaa];
let suffix = [0xbb];

assert!(is_supported_runtime_state_type(&abi, &TypeArtifact::Struct { name: "Outer".to_string() }));
let contract = abi.contracts.get_mut("Foo").unwrap();
contract.runtime_state = runtime_state;
contract.compiled.bytecode = [prefix.as_slice(), state.as_slice(), suffix.as_slice()].concat();
contract.compiled.template_hash = template_hash(&prefix, &suffix);
contract.compiled.state_span = StateSpanArtifact { offset: prefix.len(), len: state.len() };

abi.structs.get_mut("Inner").unwrap().fields[0].ty = TypeArtifact::Bytes;
assert!(!is_supported_runtime_state_type(&abi, &TypeArtifact::Struct { name: "Outer".to_string() }));
abi.verify().expect("canonical variable-width runtime state verifies");
}

abi.structs.get_mut("Inner").unwrap().fields[0].ty = TypeArtifact::Struct { name: "Outer".to_string() };
assert!(!is_supported_runtime_state_type(&abi, &TypeArtifact::Struct { name: "Outer".to_string() }));
#[test]
fn rejects_cyclic_runtime_state_types_during_decoding() {
let mut abi = tiny_sil_abi();
abi.structs.insert(
"Cycle".to_string(),
StructArtifact {
fields: vec![FieldArtifact { name: "next".to_string(), ty: TypeArtifact::Struct { name: "Cycle".to_string() } }],
},
);
abi.contracts.get_mut("Foo").unwrap().runtime_state.fields =
vec![RuntimeFieldArtifact { name: "cycle".to_string(), ty: TypeArtifact::Struct { name: "Cycle".to_string() } }];

assert!(matches!(
abi.verify(),
Err(SilAbiVerificationError::InvalidRuntimeStateEncoding { ref contract, ref message })
if contract == "Foo" && message.contains("cyclic struct Cycle")
));
}

#[test]
Expand Down
18 changes: 4 additions & 14 deletions silverscript-lang/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1280,13 +1280,7 @@ fn format_state_object(fields: &[StateFieldExpr<'_>]) -> String {
}

fn format_string_literal(value: &str) -> String {
if !value.contains('"') {
return format!("\"{value}\"");
}
if !value.contains('\'') {
return format!("'{value}'");
}
format!("\"{}\"", value.replace('"', "\\\""))
serde_json::to_string(value).expect("serializing a Rust string as a JSON string cannot fail")
}

const PREC_POSTFIX: u8 = 11;
Expand Down Expand Up @@ -2672,13 +2666,9 @@ fn parse_date_literal<'i>(pair: Pair<'i, Rule>) -> Result<Expr<'i>, CompilerErro
fn parse_string_literal<'i>(pair: Pair<'i, Rule>) -> Result<Expr<'i>, CompilerError> {
let span = Span::from(pair.as_span());
let raw = pair.as_str();
let unquoted = if (raw.starts_with('"') && raw.ends_with('"')) || (raw.starts_with('\'') && raw.ends_with('\'')) {
&raw[1..raw.len() - 1]
} else {
raw
};
let unescaped = unquoted.replace("\\\"", "\"").replace("\\'", "'");
Ok(Expr::new(ExprKind::String(unescaped), span))
let value =
serde_json::from_str::<String>(raw).map_err(|err| CompilerError::InvalidLiteral(format!("invalid string literal: {err}")))?;
Ok(Expr::new(ExprKind::String(value), span))
}

fn parse_introspection<'i>(raw: &str, span: Span<'i>) -> Result<Expr<'i>, CompilerError> {
Expand Down
4 changes: 3 additions & 1 deletion silverscript-lang/src/silverscript.pest
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ ExponentPart = { ("e" | "E") ~ NumberPart }



StringLiteral = @{ "\"" ~ ("\\\"" | !("\"" | "\r" | "\n") ~ ANY)* ~ "\"" | "'" ~ ("\\'" | !("'" | "\r" | "\n") ~ ANY)* ~ "'" }
StringLiteral = @{
"\"" ~ (("\\" ~ !("\r" | "\n") ~ ANY) | !("\"" | "\r" | "\n") ~ ANY)* ~ "\""
}

DateLiteral = { "date(" ~ StringLiteral ~ ")" }

Expand Down
77 changes: 76 additions & 1 deletion silverscript-lang/tests/ast_format_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use silverscript_lang::ast::{Expr, Span, format_contract_ast, parse_contract_ast};
use silverscript_lang::ast::{Expr, ExprKind, Span, format_contract_ast, parse_contract_ast, parse_expression_ast};
use silverscript_lang::compiler::{CompileOptions, compile_contract, compile_contract_ast, sil_abi_artifact_from_compiled};

fn assert_compiled_formatted_contract_preserves_ast(source: &str, options: CompileOptions) {
Expand All @@ -12,6 +12,81 @@ fn assert_compiled_formatted_contract_preserves_ast(source: &str, options: Compi
);
}

fn parsed_string(source: &str) -> String {
match parse_expression_ast(source).expect("string expression parses").kind {
ExprKind::String(value) => value,
kind => panic!("expected string expression, got {kind:?}"),
}
}

#[test]
fn rejects_single_quoted_string_literals() {
for source in ["'text'", r#"'It\'s working'"#] {
assert!(parse_expression_ast(source).is_err(), "single-quoted string should fail: {source}");
}
}

#[test]
fn documented_newline_escape_decodes_to_one_byte() {
let value = parsed_string(r#""\n""#);
assert_eq!(value.as_bytes(), &[0x0a]);
}

#[test]
fn string_parser_decodes_complete_json_escape_matrix() {
let value = parsed_string(r#""\b\f\n\r\t\\\/\"\u0041\u00df\u6771\uD834\uDD1E""#);
assert_eq!(value, "\u{8}\u{c}\n\r\t\\/\"Aß東𝄞");
assert_eq!(parsed_string(r#""ends\\""#), "ends\\");
}

#[test]
fn string_parser_rejects_malformed_json_escapes() {
for source in [r#""\x41""#, r#""\u12""#, r#""\uD800""#] {
assert!(parse_expression_ast(source).is_err(), "malformed escape should fail: {source}");
}
}

#[test]
fn string_formatter_round_trips_decoded_escapes() {
let source = r#"
contract Escapes() {
entry main() {
string value = "line\n\t\"quoted\"\\end\\";
require(value.length > 0, "message\r\n");
}
}
"#;
let ast = parse_contract_ast(source).expect("escaped contract parses");
let formatted = format_contract_ast(&ast);
let reparsed = parse_contract_ast(&formatted).expect("formatted escaped contract reparses");

assert!(formatted.contains(r#"\n\t\"quoted\"\\end\\"#));
assert_eq!(format_contract_ast(&reparsed), formatted);
}

#[test]
fn formatted_string_preserves_a_literal_backslash_before_quote() {
let source = r#"
contract Escapes() {
string constant VALUE = "\\\"";

entry main() {
require(VALUE.length == 2);
}
}
"#;
let ast = parse_contract_ast(source).expect("source parses");
let formatted = format_contract_ast(&ast);
let reparsed = parse_contract_ast(&formatted).expect("formatted source parses");

let ExprKind::String(original) = &ast.constants[0].expr.kind else { panic!("original constant must be a string") };
let ExprKind::String(round_tripped) = &reparsed.constants[0].expr.kind else { panic!("reparsed constant must be a string") };
assert_eq!(
round_tripped, original,
"formatting must not consume a literal backslash before a quote; formatted source:\n{formatted}"
);
}

#[test]
fn formats_contract_ast_into_canonical_silverscript() {
let source = r#"
Expand Down
35 changes: 35 additions & 0 deletions silverscript-lang/tests/compiler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,22 @@ fn portable_abi_verifies_struct_contract_field_state_layout() {
abi.verify().expect("portable ABI runtime-state metadata matches the flattened state span");
}

#[test]
fn portable_abi_verifies_and_executes_dynamic_string_state() {
let source = r#"
contract VariableState(string initial) {
string stored = initial;
entry main() { require(stored == "hello"); }
}
"#;

let abi = compile_to_sil_abi_artifact(source, &["hello".into()]).expect("dynamic string state compiles");
abi.verify().expect("compiler output with dynamic string state verifies");
let sigscript = encode_single_entry_sig_script(&abi, &[]).expect("sigscript builds");

run_bytecode_with_sigscript(bytecode(&abi), sigscript).expect("verified dynamic string state executes");
}

#[test]
fn constructor_arguments_are_concrete_values_not_runtime_introspection() {
let source = r#"
Expand Down Expand Up @@ -7552,6 +7568,25 @@ fn compiles_contract_constants_and_verifies() {
assert!(run_bytecode_with_dispatch_tag(bytecode(&compiled), dispatch_tag).is_ok());
}

#[test]
fn runtime_string_constant_preserves_literal_backslash_before_quote() {
let source = r#"
contract Escapes() {
string constant VALUE = "\\\"";

entry main() {
require(VALUE.length == 2);
}
}
"#;

let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds");
let sigscript = encode_single_entry_sig_script(&compiled, &[]).expect("sigscript builds");

run_bytecode_with_sigscript(bytecode(&compiled), sigscript)
.expect("a literal backslash followed by a quote remains two bytes at runtime");
}

#[test]
fn compiles_contract_fields_as_script_prolog() {
let source = r#"
Expand Down
2 changes: 1 addition & 1 deletion silverscript-lang/tests/examples/announcement.sil
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ contract Announcement() {
entry announce() {
// TODO: Change this to check the payload field.
byte[] announcement = byte[](0x00006a02026d4c62)
+ byte[]('A contract may not injure a human being or, through inaction, allow a human being to come to harm.');
+ byte[]("A contract may not injure a human being or, through inaction, allow a human being to come to harm.");

require(tx.outputs[0].value == 0);
require(tx.outputs[0].scriptPubKey == announcement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ contract Test(int x) {
require(myOtherVariable > x);

string x1 = "Hello \n \\ ' '' \" World";
string x2 = 'Hello \n \\ " " \' World';
string x2 = "Hello \n \\ \" \" ' World";
require(sha256(byte[](x1)) == sha256(byte[](x2)));
}
}
2 changes: 1 addition & 1 deletion silverscript-lang/tests/examples/trailing_comma.sil
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ contract Contract(
datasig oracleMsgSig,
sig oracleTxSig,
) {
byte[] oracleMessage = byte[]('Spend') + (12 as byte[8]);
byte[] oracleMessage = byte[]("Spend") + (12 as byte[8]);
require(checkMsgSig(
oracleMsgSig,
sha256(oracleMessage),
Expand Down