diff --git a/src/abi/antelope.rs b/src/abi/antelope.rs new file mode 100644 index 000000000..feb8bfe98 --- /dev/null +++ b/src/abi/antelope.rs @@ -0,0 +1,720 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::sema::ast::{Namespace, Type}; +use num_traits::ToPrimitive; +use serde::Serialize; +use solang_parser::pt::FunctionTy; + +/// Normalize a Solidity identifier into a valid eosio::name-compatible action name. +/// +/// eosio::name only admits the charset `.`, `1`-`5`, `a`-`z` and at most 13 chars +/// (12 used here for the 5-bit region). We therefore: +/// 1. lowercase first — eosio::name has no uppercase, so `putHash` must become +/// `puthash`, NOT `putash` (dropping the uppercase byte silently mangles the +/// name and can collide two functions); +/// 2. keep only the eosio charset — digits `0` and `6`-`9` are NOT valid +/// eosio::name characters, so `putu8` normalizes to `putu`, matching what +/// nodeos itself would do at `set abi` time; +/// 3. truncate to 12 characters. +/// +/// This is the single authority for action-name derivation: the ABI's +/// `actions[].name` (here) and the emitter's dispatch switch (`emit_apply`) must +/// agree exactly, or dispatch silently fails to route. Event names follow the +/// same charset in `codegen/events/antelope.rs`. Lives in the always-compiled +/// `abi` module so `sema` can reach it without depending on the llvm-gated `emit`. +pub fn normalize_action_name(name: &str) -> String { + name.chars() + .flat_map(char::to_lowercase) + .filter(|c| c.is_ascii_lowercase() || matches!(c, '1'..='5') || *c == '.') + .take(12) + .collect() +} + +/// Type id used in `abi_extensions` to mark a Solang storage-layout blob. +/// ASCII for 'S','L' (Solang). The payload is UTF-8 JSON; see [`AntelopeLayout`]. +pub const SOLANG_LAYOUT_EXT_TYPE: u16 = 0x534C; + +/// Schema version for the layout JSON. +pub const SOLANG_LAYOUT_VERSION: u32 = 1; + +#[derive(Serialize)] +pub struct AntelopeAbi { + pub version: String, + pub types: Vec, + pub structs: Vec, + pub actions: Vec, + pub tables: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub abi_extensions: Vec, +} + +#[derive(Serialize)] +pub struct AbiStruct { + pub name: String, + pub base: String, + pub fields: Vec, +} + +#[derive(Serialize)] +pub struct AbiField { + pub name: String, + #[serde(rename = "type")] + pub ty: String, +} + +#[derive(Serialize)] +pub struct AbiAction { + pub name: String, + #[serde(rename = "type")] + pub ty: String, + pub ricardian_contract: String, +} + +#[derive(Serialize)] +pub struct AbiTable { + pub name: String, + pub index_type: String, + pub key_names: Vec, + pub key_types: Vec, + #[serde(rename = "type")] + pub ty: String, +} + +/// One entry of the standard `abi_extensions: pair[]` array. +/// `data` is the hex-encoded payload (the standard wire form for the `bytes` field). +/// +/// fc's `extensions_type` is `vector>>`, whose JSON +/// representation is a 2-element tuple `[type, dataHex]` — NOT an object +/// `{"type":…,"data":…}`. nodeos/fc cannot parse the object form and rejects +/// `cleos set abi` with "Bad Cast: Invalid cast from object_type to Array", so we +/// serialize as a tuple. (The struct keeps named fields for ergonomic construction +/// and testing; only the wire form is a tuple.) +pub struct AbiExtension { + pub ty: u16, + pub data: String, +} + +impl Serialize for AbiExtension { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeTuple; + let mut tup = serializer.serialize_tuple(2)?; + tup.serialize_element(&self.ty)?; + tup.serialize_element(&self.data)?; + tup.end() + } +} + +/// Solang-private storage layout descriptor, packed into `abi_extensions`. +/// Documents the slot/type of every state variable so clients can derive +/// idx256 keys and decode raw row bytes without re-reading the source. +/// +/// JSON shape: +/// ```json +/// { +/// "version": 1, +/// "state_table": "state", +/// "vars": [ +/// { "name": "owner", "slot": 0, "kind": "plain", "type": "uint64" }, +/// { "name": "balances", "slot": 2, "kind": "map", "keys": ["uint64"], "type": "uint256" }, +/// { "name": "users", "slot": 4, "kind": "map", "keys": ["uint64"], +/// "type": { "kind": "struct", "fields": [ +/// { "name": "balance", "type": "uint64" }, +/// { "name": "score", "type": "uint64" } +/// ]} +/// } +/// ] +/// } +/// ``` +#[derive(Serialize)] +pub struct AntelopeLayout { + pub version: u32, + pub state_table: String, + pub vars: Vec, +} + +#[derive(Serialize)] +pub struct LayoutVar { + pub name: String, + pub slot: u64, + /// One of: "plain", "map", "unsupported". Arrays/dynamic-length compound + /// values are emitted as `"unsupported"` for now — readers must skip them. + pub kind: String, + /// Only present when `kind == "map"`. Lists the chain of key types, + /// outermost first. Nested mappings produce multiple entries. + #[serde(skip_serializing_if = "Option::is_none")] + pub keys: Option>, + #[serde(rename = "type")] + pub ty: LayoutType, +} + +/// Value type. Strings denote primitives ("uint64", "address", "string", ...). +/// Objects describe compound shapes (struct, array, unsupported). +#[derive(Serialize)] +#[serde(untagged)] +pub enum LayoutType { + Primitive(String), + Compound(LayoutCompound), +} + +#[derive(Serialize)] +#[serde(tag = "kind")] +pub enum LayoutCompound { + #[serde(rename = "struct")] + Struct { fields: Vec }, + #[serde(rename = "array")] + Array { element: Box }, + #[serde(rename = "unsupported")] + Unsupported { reason: String }, +} + +#[derive(Serialize)] +pub struct LayoutField { + pub name: String, + #[serde(rename = "type")] + pub ty: LayoutType, +} + +fn solidity_type_to_antelope(ty: &Type, _ns: &Namespace) -> String { + match ty { + Type::Uint(8) => "uint8".to_string(), + Type::Uint(16) => "uint16".to_string(), + Type::Uint(32) => "uint32".to_string(), + Type::Uint(64) => "uint64".to_string(), + Type::Uint(128) => "uint128".to_string(), + Type::Uint(256) => "checksum256".to_string(), + Type::Int(8) => "int8".to_string(), + Type::Int(16) => "int16".to_string(), + Type::Int(32) => "int32".to_string(), + Type::Int(64) => "int64".to_string(), + Type::Int(128) => "int128".to_string(), + Type::Bool => "bool".to_string(), + Type::String => "string".to_string(), + // A 32-byte value (e.g. a sha256/keccak hash) is a fixed 32 raw bytes with + // no length prefix — that is exactly `checksum256`. Mapping it to `bytes` + // (which carries a varuint32 length prefix) would misalign action data. + Type::Bytes(32) => "checksum256".to_string(), + Type::Address(_) => "uint64".to_string(), + _ => "bytes".to_string(), + } +} + +/// Render a Solang `Type` as the layout-JSON primitive name. +/// Mirrors Solidity's own type names rather than the ABI's Antelope renaming +/// (e.g. `address`, not `uint64`) because the reader does its own type-aware +/// decoding from the raw row bytes. +fn primitive_name(ty: &Type) -> Option { + Some(match ty { + Type::Bool => "bool".to_string(), + Type::Uint(w) => format!("uint{w}"), + Type::Int(w) => format!("int{w}"), + Type::Address(_) => "address".to_string(), + Type::String => "string".to_string(), + Type::DynamicBytes => "bytes".to_string(), + Type::Bytes(n) => format!("bytes{n}"), + _ => return None, + }) +} + +fn type_to_layout(ty: &Type, ns: &Namespace) -> LayoutType { + if let Some(p) = primitive_name(ty) { + return LayoutType::Primitive(p); + } + match ty { + Type::Struct(st) => { + let decl = st.definition(ns); + let fields = decl + .fields + .iter() + .map(|f| LayoutField { + name: f.id.as_ref().map(|i| i.name.clone()).unwrap_or_default(), + ty: type_to_layout(&f.ty, ns), + }) + .collect(); + LayoutType::Compound(LayoutCompound::Struct { fields }) + } + Type::Enum(_) => LayoutType::Primitive("uint8".to_string()), + Type::Array(elem, _) => LayoutType::Compound(LayoutCompound::Array { + element: Box::new(type_to_layout(elem, ns)), + }), + // Mappings appear as the value of a map only when nested — handled by + // `extract_map_keys`. Any other case is something the reader can't decode. + other => LayoutType::Compound(LayoutCompound::Unsupported { + reason: format!("{other:?}"), + }), + } +} + +/// Walk through nested `Mapping` types, collecting key type names and returning +/// the innermost value type. Returns (keys, value_ty). If `ty` is not a mapping, +/// returns an empty key vec and `ty` unchanged. +fn extract_map_keys<'a>(ty: &'a Type) -> (Vec, &'a Type) { + let mut keys = Vec::new(); + let mut cur = ty; + while let Type::Mapping(m) = cur { + keys.push(primitive_name(&m.key).unwrap_or_else(|| "bytes".to_string())); + cur = &m.value; + } + (keys, cur) +} + +/// Build the storage-layout descriptor for a contract. +pub fn gen_layout(contract_no: usize, ns: &Namespace) -> AntelopeLayout { + let contract = &ns.contracts[contract_no]; + let mut vars = Vec::new(); + + for entry in &contract.layout { + // Solang's slot allocator hands out small dense indices; if a contract ever + // declares enough variables to overflow u64, drop the entry — the reader + // would not be able to encode it as a varuint anyway. + let slot = match entry.slot.to_u64() { + Some(s) => s, + None => continue, + }; + + let base_var = &ns.contracts[entry.contract_no].variables[entry.var_no]; + let (keys, value_ty) = extract_map_keys(&entry.ty); + let value_layout = type_to_layout(value_ty, ns); + + let kind = if keys.is_empty() { + "plain" + } else { + "map" + } + .to_string(); + + vars.push(LayoutVar { + name: base_var.name.clone(), + slot, + kind, + keys: if keys.is_empty() { None } else { Some(keys) }, + ty: value_layout, + }); + } + + AntelopeLayout { + version: SOLANG_LAYOUT_VERSION, + state_table: "state".to_string(), + vars, + } +} + +/// Generate Antelope ABI for a contract. +pub fn gen_abi(contract_no: usize, ns: &Namespace) -> AntelopeAbi { + let contract = &ns.contracts[contract_no]; + let mut structs = Vec::new(); + let mut actions = Vec::new(); + + // For each public function, create a struct (for action params) and an action entry. + for func_no in contract.all_functions.keys() { + let func = &ns.functions[*func_no]; + + if !func.is_public() { + continue; + } + if func.ty == FunctionTy::Constructor { + continue; + } + + let func_name = &func.id.name; + + // Antelope action names are max 12 chars, lowercase + 1-5 + dot. + // Shared normalizer keeps this identical to the emitter's dispatch switch. + let action_name = normalize_action_name(func_name); + + if action_name.is_empty() { + continue; + } + + // Build the struct fields from function parameters. + let fields: Vec = func + .params + .iter() + .enumerate() + .map(|(i, p)| AbiField { + name: p + .id + .as_ref() + .map(|id| id.name.clone()) + .unwrap_or_else(|| format!("arg{i}")), + ty: solidity_type_to_antelope(&p.ty, ns), + }) + .collect(); + + structs.push(AbiStruct { + name: action_name.clone(), + base: String::new(), + fields, + }); + + actions.push(AbiAction { + name: action_name.clone(), + ty: action_name, + ricardian_contract: String::new(), + }); + } + + // Add the "state" table entry so explorers can decode storage. + // Storage model: auto-increment primary key, idx256 secondary index for slot lookup. + // Row data = raw value bytes (variable length: uint64, uint256, strings, etc.). + let has_state_vars = !contract.variables.iter().all(|v| v.constant); + let mut tables = Vec::new(); + + if has_state_vars { + structs.push(AbiStruct { + name: "state.row".to_string(), + base: String::new(), + fields: vec![ + AbiField { + name: "id".to_string(), + ty: "uint64".to_string(), + }, + AbiField { + name: "key".to_string(), + ty: "checksum256".to_string(), + }, + AbiField { + name: "value".to_string(), + ty: "bytes".to_string(), + }, + ], + }); + + tables.push(AbiTable { + name: "state".to_string(), + index_type: "i64".to_string(), + key_names: vec![], + key_types: vec![], + ty: "state.row".to_string(), + }); + } + + // Emit the storage-layout descriptor inside abi_extensions so it survives + // the on-chain `setabi` round-trip. Only emit when there's something to say. + let mut abi_extensions = Vec::new(); + if has_state_vars { + let layout = gen_layout(contract_no, ns); + if !layout.vars.is_empty() { + let json = serde_json::to_vec(&layout) + .expect("AntelopeLayout always serializes to JSON"); + abi_extensions.push(AbiExtension { + ty: SOLANG_LAYOUT_EXT_TYPE, + data: hex::encode(json), + }); + } + } + + AntelopeAbi { + version: "eosio::abi/1.2".to_string(), + types: Vec::new(), + structs, + actions, + tables, + abi_extensions, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::{codegen, Options}; + use crate::file_resolver::FileResolver; + use crate::{parse_and_resolve, Target}; + use std::ffi::OsStr; + + fn build_ns(src: &str) -> (crate::sema::ast::Namespace, usize) { + let mut resolver = FileResolver::default(); + resolver.set_file_contents("test.sol", src.to_string()); + let mut ns = parse_and_resolve(OsStr::new("test.sol"), &mut resolver, Target::Antelope); + assert!( + !ns.diagnostics.any_errors(), + "sema reported errors compiling test contract" + ); + // Codegen populates `contract.layout` — required for gen_layout. + codegen(&mut ns, &Options::default()); + assert!(!ns.contracts.is_empty(), "no contracts compiled"); + // The deepest derived contract is the last one — its layout reflects inheritance. + let contract_no = ns.contracts.len() - 1; + (ns, contract_no) + } + + fn compile_to_layout(src: &str) -> AntelopeLayout { + let (ns, contract_no) = build_ns(src); + gen_layout(contract_no, &ns) + } + + fn compile_to_abi(src: &str) -> AntelopeAbi { + let (ns, contract_no) = build_ns(src); + gen_abi(contract_no, &ns) + } + + #[test] + fn layout_plain_vars_get_dense_slots_and_primitive_types() { + let layout = compile_to_layout( + r#" + contract C { + uint64 a; + uint256 b; + bool c; + address d; + string e; + } + "#, + ); + + assert_eq!(layout.version, SOLANG_LAYOUT_VERSION); + assert_eq!(layout.state_table, "state"); + + let names: Vec<&str> = layout.vars.iter().map(|v| v.name.as_str()).collect(); + assert_eq!(names, vec!["a", "b", "c", "d", "e"]); + + // Slot 0 is always the first declared variable. + assert_eq!(layout.vars[0].slot, 0); + // Plain variables always have kind="plain" and no `keys` entry. + for v in &layout.vars { + assert_eq!(v.kind, "plain"); + assert!(v.keys.is_none()); + } + + // Primitive types are serialized as bare strings. + let types: Vec = layout + .vars + .iter() + .map(|v| match &v.ty { + LayoutType::Primitive(s) => s.clone(), + _ => panic!("expected primitive"), + }) + .collect(); + assert_eq!(types, vec!["uint64", "uint256", "bool", "address", "string"]); + } + + #[test] + fn layout_mapping_captures_key_chain() { + let layout = compile_to_layout( + r#" + contract C { + mapping(uint64 => uint256) balances; + mapping(uint64 => mapping(uint64 => uint256)) allowed; + } + "#, + ); + + assert_eq!(layout.vars.len(), 2); + + let bal = &layout.vars[0]; + assert_eq!(bal.name, "balances"); + assert_eq!(bal.kind, "map"); + assert_eq!(bal.keys.as_deref(), Some(&["uint64".to_string()][..])); + match &bal.ty { + LayoutType::Primitive(s) => assert_eq!(s, "uint256"), + _ => panic!("expected primitive uint256"), + } + + let allowed = &layout.vars[1]; + assert_eq!(allowed.name, "allowed"); + assert_eq!(allowed.kind, "map"); + assert_eq!( + allowed.keys.as_deref(), + Some(&["uint64".to_string(), "uint64".to_string()][..]) + ); + } + + #[test] + fn layout_mapping_to_struct_expands_fields() { + let layout = compile_to_layout( + r#" + contract C { + struct User { uint64 balance; uint64 score; } + mapping(uint64 => User) users; + } + "#, + ); + + assert_eq!(layout.vars.len(), 1); + let users = &layout.vars[0]; + assert_eq!(users.name, "users"); + assert_eq!(users.kind, "map"); + + match &users.ty { + LayoutType::Compound(LayoutCompound::Struct { fields }) => { + assert_eq!(fields.len(), 2); + assert_eq!(fields[0].name, "balance"); + assert_eq!(fields[1].name, "score"); + match (&fields[0].ty, &fields[1].ty) { + (LayoutType::Primitive(a), LayoutType::Primitive(b)) => { + assert_eq!(a, "uint64"); + assert_eq!(b, "uint64"); + } + _ => panic!("expected primitive fields"), + } + } + _ => panic!("expected struct value"), + } + } + + #[test] + fn layout_enum_serializes_as_uint8() { + let layout = compile_to_layout( + r#" + contract C { + enum Status { Open, Closed, Frozen } + Status s; + } + "#, + ); + assert_eq!(layout.vars.len(), 1); + match &layout.vars[0].ty { + LayoutType::Primitive(s) => assert_eq!(s, "uint8"), + _ => panic!("expected primitive"), + } + } + + #[test] + fn layout_dynamic_array_marked_array() { + let layout = compile_to_layout( + r#" + contract C { + uint64[] xs; + } + "#, + ); + assert_eq!(layout.vars.len(), 1); + match &layout.vars[0].ty { + LayoutType::Compound(LayoutCompound::Array { element }) => match element.as_ref() { + LayoutType::Primitive(s) => assert_eq!(s, "uint64"), + _ => panic!("expected primitive element"), + }, + _ => panic!("expected array"), + } + } + + #[test] + fn layout_inherited_vars_get_correct_slots() { + // Both base + derived variables should appear, in declaration order. + let layout = compile_to_layout( + r#" + contract Base { uint64 baseVar; } + contract Child is Base { uint64 childVar; } + "#, + ); + let names: Vec<&str> = layout.vars.iter().map(|v| v.name.as_str()).collect(); + assert_eq!(names, vec!["baseVar", "childVar"]); + assert_eq!(layout.vars[0].slot, 0); + // baseVar takes one slot, so childVar starts at slot 1. + assert_eq!(layout.vars[1].slot, 1); + } + + #[test] + fn layout_constants_are_excluded() { + let layout = compile_to_layout( + r#" + contract C { + uint64 constant K = 42; + uint64 v; + } + "#, + ); + // The constant must not appear; only `v` is stored. + assert_eq!(layout.vars.len(), 1); + assert_eq!(layout.vars[0].name, "v"); + assert_eq!(layout.vars[0].slot, 0); + } + + #[test] + fn abi_includes_layout_in_abi_extensions() { + let abi = compile_to_abi( + r#" + contract C { + uint64 owner; + mapping(uint64 => uint256) balances; + } + "#, + ); + + assert_eq!(abi.abi_extensions.len(), 1); + let ext = &abi.abi_extensions[0]; + assert_eq!(ext.ty, SOLANG_LAYOUT_EXT_TYPE); + + // C5: the wire form must be a 2-element tuple `[type, dataHex]`, not an + // object `{"type":…,"data":…}` — fc's extensions_type is + // vector> and nodeos rejects the object form. + let wire: serde_json::Value = + serde_json::to_value(&abi.abi_extensions).expect("abi_extensions serializes"); + let entry = &wire[0]; + assert!( + entry.is_array(), + "abi_extensions entry must be a tuple array, got: {entry}" + ); + assert_eq!(entry[0], SOLANG_LAYOUT_EXT_TYPE); + assert_eq!(entry[1], serde_json::Value::String(ext.data.clone())); + + // Decode and re-parse the layout JSON to verify the on-chain round-trip. + // Use serde_json::Value so we don't have to add Deserialize impls just for the test. + let bytes = hex::decode(&ext.data).expect("data must be hex"); + let parsed: serde_json::Value = + serde_json::from_slice(&bytes).expect("data must be valid layout JSON"); + + assert_eq!(parsed["version"], SOLANG_LAYOUT_VERSION); + assert_eq!(parsed["state_table"], "state"); + let vars = parsed["vars"].as_array().expect("vars must be an array"); + assert_eq!(vars.len(), 2); + assert_eq!(vars[0]["name"], "owner"); + assert_eq!(vars[1]["name"], "balances"); + assert_eq!(vars[1]["kind"], "map"); + assert_eq!(vars[1]["keys"][0], "uint64"); + assert_eq!(vars[1]["type"], "uint256"); + } + + #[test] + fn normalize_action_name_lowercases_and_restricts_charset() { + // C1: camelCase lowercases (not drops uppercase) — `putHash` → `puthash`, + // NOT the old buggy `putash`/`setorker`. + assert_eq!(normalize_action_name("putHash"), "puthash"); + assert_eq!(normalize_action_name("setWorker"), "setworker"); + // Truncated to 12 chars. + assert_eq!(normalize_action_name("averylongfunctionname"), "averylongfun"); + } + + #[test] + fn abi_action_names_are_lowercased_not_dropped() { + // C1: an action named with camelCase must lowercase, not drop uppercase. + let abi = compile_to_abi( + r#" + contract C { + uint64 x; + function putHash(uint64 v) public { x = v; } + function setWorker(uint64 v) public { x = v; } + } + "#, + ); + let names: Vec<&str> = abi.actions.iter().map(|a| a.name.as_str()).collect(); + assert!(names.contains(&"puthash"), "actions: {names:?}"); + assert!(names.contains(&"setworker"), "actions: {names:?}"); + } + + #[test] + fn abi_action_names_restrict_digits_to_eosio_charset() { + // C2: digits 0 and 6-9 are not valid eosio::name chars and are dropped; + // 1-5 are kept. `putu8` → `putu`, `slot12` → `slot12`. + assert_eq!(normalize_action_name("putu8"), "putu"); + assert_eq!(normalize_action_name("slot12"), "slot12"); + assert_eq!(normalize_action_name("get9"), "get"); + } + + #[test] + fn abi_omits_extensions_when_no_state_vars() { + // A contract with only constants has nothing to describe in the layout — + // the extension array should be empty. + let abi = compile_to_abi( + r#" + contract C { + uint64 constant K = 7; + function get() public pure returns (uint64) { return K; } + } + "#, + ); + assert!(abi.abi_extensions.is_empty()); + } +} diff --git a/src/abi/mod.rs b/src/abi/mod.rs index c745eb815..01d3ca063 100644 --- a/src/abi/mod.rs +++ b/src/abi/mod.rs @@ -4,6 +4,7 @@ use crate::sema::ast::Namespace; use crate::Target; pub mod anchor; +pub mod antelope; pub mod ethereum; pub mod polkadot; mod tests; @@ -41,6 +42,18 @@ pub fn generate_abi( (serde_json::to_string_pretty(&idl).unwrap(), "json") } + Target::Antelope => { + if verbose { + eprintln!( + "info: Generating Antelope ABI for contract {}", + ns.contracts[contract_no].id + ); + } + + let abi = antelope::gen_abi(contract_no, ns); + + (serde_json::to_string_pretty(&abi).unwrap(), "abi") + } _ => { if verbose { eprintln!( diff --git a/src/bin/cli/mod.rs b/src/bin/cli/mod.rs index 67839d9a2..1e9587a41 100644 --- a/src/bin/cli/mod.rs +++ b/src/bin/cli/mod.rs @@ -50,7 +50,7 @@ pub enum Commands { #[derive(Args)] pub struct New { - #[arg(name = "TARGETNAME",required= true, long = "target", value_parser = ["solana", "polkadot", "evm"], help = "Target to build for [possible values: solana, polkadot]", num_args = 1, hide_possible_values = true)] + #[arg(name = "TARGETNAME",required= true, long = "target", value_parser = ["solana", "polkadot", "evm", "antelope"], help = "Target to build for [possible values: solana, polkadot]", num_args = 1, hide_possible_values = true)] pub target_name: String, #[arg(name = "INPUT", help = "Name of the project", num_args = 1, value_parser = ValueParser::os_string())] @@ -261,7 +261,7 @@ pub struct CompilerOutput { #[derive(Args)] pub struct TargetArg { - #[arg(name = "TARGET",required= true, long = "target", value_parser = ["solana", "polkadot", "evm"], help = "Target to build for [possible values: solana, polkadot]", num_args = 1, hide_possible_values = true)] + #[arg(name = "TARGET",required= true, long = "target", value_parser = ["solana", "polkadot", "evm", "antelope"], help = "Target to build for [possible values: solana, polkadot]", num_args = 1, hide_possible_values = true)] pub name: String, #[arg(name = "ADDRESS_LENGTH", help = "Address length on the Polkadot Parachain", long = "address-length", num_args = 1, value_parser = value_parser!(u64).range(4..1024))] @@ -273,7 +273,7 @@ pub struct TargetArg { #[derive(Args, Deserialize, Debug, PartialEq)] pub struct CompileTargetArg { - #[arg(name = "TARGET", long = "target", value_parser = ["solana", "polkadot", "evm", "soroban"], help = "Target to build for [possible values: solana, polkadot]", num_args = 1, hide_possible_values = true)] + #[arg(name = "TARGET", long = "target", value_parser = ["solana", "polkadot", "evm", "soroban", "antelope"], help = "Target to build for [possible values: solana, polkadot]", num_args = 1, hide_possible_values = true)] pub name: Option, #[arg(name = "ADDRESS_LENGTH", help = "Address length on the Polkadot Parachain", long = "address-length", num_args = 1, value_parser = value_parser!(u64).range(4..1024))] @@ -449,7 +449,7 @@ impl TargetArgTrait for CompileTargetArg { pub(crate) fn target_arg(target_arg: &T) -> Target { let target_name = target_arg.get_name(); - if target_name == "solana" || target_name == "evm" { + if target_name == "solana" || target_name == "evm" || target_name == "antelope" { if target_arg.get_address_length().is_some() { eprintln!("error: address length cannot be modified except for polkadot target"); exit(1); @@ -469,6 +469,7 @@ pub(crate) fn target_arg(target_arg: &T) -> Target { }, "evm" => solang::Target::EVM, "soroban" => solang::Target::Soroban, + "antelope" => solang::Target::Antelope, _ => unreachable!(), }; diff --git a/src/codegen/dispatch/mod.rs b/src/codegen/dispatch/mod.rs index be718d9ec..f704370f8 100644 --- a/src/codegen/dispatch/mod.rs +++ b/src/codegen/dispatch/mod.rs @@ -19,5 +19,12 @@ pub(super) fn function_dispatch( polkadot::function_dispatch(contract_no, all_cfg, ns, opt) } Target::Soroban => soroban::function_dispatch(contract_no, all_cfg, ns, opt), + // Antelope's entry point is the hand-written `apply` (see emit/antelope), which + // dispatches actions by name — it does not use these selector-based dispatch CFGs. + // Generating the Polkadot ones only emitted dead functions whose Polkadot-specific + // terminators (ReturnCode / the ReturnData success path) Antelope's emit doesn't + // lower, leaving blocks unterminated → invalid IR that crashed the backend at + // -O none/less (masked at -O default only because global_dce drops dead functions). + Target::Antelope => vec![], } } diff --git a/src/codegen/events/antelope.rs b/src/codegen/events/antelope.rs new file mode 100644 index 000000000..695c83752 --- /dev/null +++ b/src/codegen/events/antelope.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::codegen::cfg::{ControlFlowGraph, Instr}; +use crate::codegen::encoding::abi_encode; +use crate::codegen::events::EventEmitter; +use crate::codegen::expression::expression; +use crate::codegen::vartable::Vartable; +use crate::codegen::{Expression, Options}; +use crate::emit::antelope::string_to_name; +use crate::sema::ast::{self, Function, Namespace, Type}; +use solang_parser::pt; + +/// Antelope event emitter. +/// +/// Events are emitted as inline actions (send_inline) to the contract itself. +/// topics[0] = eosio::name(event_name) as uint64 — used as the action name. +/// data = ABI-encoded event fields — used as the action data. +pub(super) struct AntelopeEventEmitter<'a> { + pub(super) args: &'a [ast::Expression], + pub(super) ns: &'a Namespace, + pub(super) event_no: usize, +} + +impl EventEmitter for AntelopeEventEmitter<'_> { + fn selector(&self, _emitting_contract_no: usize) -> Vec { + let event = &self.ns.events[self.event_no]; + event.id.name.as_bytes().to_vec() + } + + fn emit( + &self, + contract_no: usize, + func: &Function, + cfg: &mut ControlFlowGraph, + vartab: &mut Vartable, + opt: &Options, + ) { + let loc = pt::Loc::Builtin; + let event = &self.ns.events[self.event_no]; + + // Encode the event name as eosio::name uint64 at compile time. + // This will be used as the action name in the send_inline call. + let event_name = &event.id.name; + // Antelope names: max 12 chars, lowercase + 1-5 + dot. + // Prefix with "e." to avoid collisions with real contract action names. + // E.g. event "Mint" → "e.mint", "Transfer" → "e.transfer". + let action_name: String = std::iter::once('e') + .chain(std::iter::once('.')) + .chain( + event_name + .to_lowercase() + .chars() + .filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) + ) + .take(12) + .collect(); + let name_encoded = string_to_name(&action_name); + + let topics = vec![Expression::NumberLiteral { + loc, + ty: Type::Uint(64), + value: name_encoded.into(), + }]; + + // Evaluate and ABI-encode all event fields. + let data: Vec = self + .args + .iter() + .map(|e| expression(e, cfg, contract_no, Some(func), self.ns, vartab, opt)) + .collect(); + + let encoded_data = if data.is_empty() { + Expression::AllocDynamicBytes { + loc, + ty: Type::DynamicBytes, + size: Expression::NumberLiteral { + loc, + ty: Type::Uint(32), + value: 0.into(), + } + .into(), + initializer: Some(Vec::new()), + } + } else { + abi_encode(&loc, data, self.ns, vartab, cfg, false).0 + }; + + cfg.add( + vartab, + Instr::EmitEvent { + event_no: self.event_no, + data: encoded_data, + topics, + }, + ); + } +} diff --git a/src/codegen/events/mod.rs b/src/codegen/events/mod.rs index 43e00e064..d309e44d6 100644 --- a/src/codegen/events/mod.rs +++ b/src/codegen/events/mod.rs @@ -1,9 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 +mod antelope; mod polkadot; mod solana; use crate::codegen::cfg::ControlFlowGraph; +use crate::codegen::events::antelope::AntelopeEventEmitter; use crate::codegen::events::polkadot::PolkadotEventEmitter; use crate::codegen::events::solana::SolanaEventEmitter; use crate::codegen::vartable::Vartable; @@ -52,5 +54,7 @@ pub(super) fn new_event_emitter<'a>( }), Target::Soroban => todo!(), + + Target::Antelope => Box::new(AntelopeEventEmitter { args, ns, event_no }), } } diff --git a/src/codegen/expression.rs b/src/codegen/expression.rs index 76cfcec09..4cf8618e5 100644 --- a/src/codegen/expression.rs +++ b/src/codegen/expression.rs @@ -1075,6 +1075,86 @@ pub fn expression( value, } } + ast::Expression::Builtin { + loc, + kind: ast::Builtin::AntelopeName, + args, + .. + } => { + // Constant-fold antelope.name("string") → u64 at compile time. + // Extract the string literal from the argument. + // Solang may represent string literals as BytesLiteral or AllocDynamicBytes. + let name_str = match &args[0] { + ast::Expression::BytesLiteral { value, .. } => { + String::from_utf8(value.clone()).unwrap_or_default() + } + ast::Expression::AllocDynamicBytes { + init: Some(value), .. + } => String::from_utf8(value.clone()).unwrap_or_default(), + _ => String::new(), + }; + let encoded = crate::emit::antelope::string_to_name(&name_str); + Expression::NumberLiteral { + loc: *loc, + ty: Type::Uint(64), + value: encoded.into(), + } + } + ast::Expression::Builtin { + loc, + kind: ast::Builtin::AntelopeRequireAuth, + args, + .. + } => { + let arg = expression(&args[0], cfg, contract_no, func, ns, vartab, opt); + let res = vartab.temp_anonymous(&Type::Uint(64)); + cfg.add( + vartab, + Instr::Set { + loc: *loc, + res, + expr: Expression::Builtin { + loc: *loc, + tys: vec![Type::Uint(64)], + kind: Builtin::AntelopeRequireAuth, + args: vec![arg], + }, + }, + ); + Expression::Poison + } + // Void side-effect builtins: must be wrapped in Instr::Set so LLVM emits them. + // Pattern: dummy Uint(64) return type, temp variable discarded, returns Poison. + ast::Expression::Builtin { + loc, + kind: kind @ (ast::Builtin::AntelopeCall + | ast::Builtin::AntelopeCallAuth + | ast::Builtin::AntelopeRequireRecipient + | ast::Builtin::AntelopeSetPayer + | ast::Builtin::AntelopeRequireAuth2), + args, + .. + } => { + let lowered_args: Vec = args + .iter() + .map(|a| expression(a, cfg, contract_no, func, ns, vartab, opt)) + .collect(); + let res = vartab.temp_anonymous(&Type::Uint(64)); + cfg.add( + vartab, + Instr::Set { + loc: *loc, + res, + expr: Expression::Builtin { + loc: *loc, + tys: vec![Type::Uint(64)], + kind: kind.into(), + args: lowered_args, + }, + }, + ); + Expression::Poison + } ast::Expression::Builtin { loc, tys, @@ -3827,7 +3907,7 @@ fn array_subscript( expr: Box::new(array), index: Box::new(index), }, - Target::Polkadot { .. } => Expression::Keccak256 { + Target::Polkadot { .. } | Target::Antelope => Expression::Keccak256 { loc: *loc, ty: array_ty.clone(), exprs: vec![array, index], diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 284d4b232..fac1dca11 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -1868,6 +1868,35 @@ pub enum Builtin { ExtendTtl, ExtendInstanceTtl, AccessMapping, + AntelopeRequireAuth, + AntelopeSelf, + AntelopeCode, + AntelopeName, + AntelopeRequireRecipient, + AntelopeCall, + AntelopeCallAuth, + AntelopeSetPayer, + AntelopePack, + AntelopeHasAuth, + AntelopeRequireAuth2, + AntelopeTimestamp, + AntelopeToUint64, + AntelopeToInt64, + AntelopeToUint32, + AntelopeToUint128, + AntelopeToBytes32, + AntelopeToString, + AntelopeDbFind, + AntelopeDbGet, + AntelopeDbNext, + AntelopeDbLowerbound, + AntelopeLastPk, + AntelopeDbIdx64Find, + AntelopeDbIdx64Lowerbound, + AntelopeDbIdx128Find, + AntelopeDbIdx128Lowerbound, + AntelopeDbIdx256Find, + AntelopeDbIdx256Lowerbound, } impl From<&ast::Builtin> for Builtin { @@ -1934,6 +1963,35 @@ impl From<&ast::Builtin> for Builtin { ast::Builtin::AuthAsCurrContract => Builtin::AuthAsCurrContract, ast::Builtin::ExtendTtl => Builtin::ExtendTtl, ast::Builtin::ExtendInstanceTtl => Builtin::ExtendInstanceTtl, + ast::Builtin::AntelopeRequireAuth => Builtin::AntelopeRequireAuth, + ast::Builtin::AntelopeSelf => Builtin::AntelopeSelf, + ast::Builtin::AntelopeCode => Builtin::AntelopeCode, + ast::Builtin::AntelopeName => Builtin::AntelopeName, + ast::Builtin::AntelopeRequireRecipient => Builtin::AntelopeRequireRecipient, + ast::Builtin::AntelopeCall => Builtin::AntelopeCall, + ast::Builtin::AntelopeCallAuth => Builtin::AntelopeCallAuth, + ast::Builtin::AntelopeSetPayer => Builtin::AntelopeSetPayer, + ast::Builtin::AntelopePack => Builtin::AntelopePack, + ast::Builtin::AntelopeHasAuth => Builtin::AntelopeHasAuth, + ast::Builtin::AntelopeRequireAuth2 => Builtin::AntelopeRequireAuth2, + ast::Builtin::AntelopeTimestamp => Builtin::AntelopeTimestamp, + ast::Builtin::AntelopeToUint64 => Builtin::AntelopeToUint64, + ast::Builtin::AntelopeToInt64 => Builtin::AntelopeToInt64, + ast::Builtin::AntelopeToUint32 => Builtin::AntelopeToUint32, + ast::Builtin::AntelopeToUint128 => Builtin::AntelopeToUint128, + ast::Builtin::AntelopeToBytes32 => Builtin::AntelopeToBytes32, + ast::Builtin::AntelopeToString => Builtin::AntelopeToString, + ast::Builtin::AntelopeDbFind => Builtin::AntelopeDbFind, + ast::Builtin::AntelopeDbGet => Builtin::AntelopeDbGet, + ast::Builtin::AntelopeDbNext => Builtin::AntelopeDbNext, + ast::Builtin::AntelopeDbLowerbound => Builtin::AntelopeDbLowerbound, + ast::Builtin::AntelopeLastPk => Builtin::AntelopeLastPk, + ast::Builtin::AntelopeDbIdx64Find => Builtin::AntelopeDbIdx64Find, + ast::Builtin::AntelopeDbIdx64Lowerbound => Builtin::AntelopeDbIdx64Lowerbound, + ast::Builtin::AntelopeDbIdx128Find => Builtin::AntelopeDbIdx128Find, + ast::Builtin::AntelopeDbIdx128Lowerbound => Builtin::AntelopeDbIdx128Lowerbound, + ast::Builtin::AntelopeDbIdx256Find => Builtin::AntelopeDbIdx256Find, + ast::Builtin::AntelopeDbIdx256Lowerbound => Builtin::AntelopeDbIdx256Lowerbound, _ => panic!("Builtin should not be in the cfg"), } } diff --git a/src/codegen/revert.rs b/src/codegen/revert.rs index 9ff265268..a78fb2526 100644 --- a/src/codegen/revert.rs +++ b/src/codegen/revert.rs @@ -187,8 +187,8 @@ pub(super) fn assert_failure( cfg: &mut ControlFlowGraph, vartab: &mut Vartable, ) { - // On Solana, returning the encoded arguments has no effect - if ns.target == Target::Solana || ns.target == Target::Soroban { + // On Solana/Soroban/Antelope, returning the encoded arguments has no effect + if ns.target == Target::Solana || ns.target == Target::Soroban || ns.target == Target::Antelope { cfg.add(vartab, Instr::AssertFailure { encoded_args: None }); return; } @@ -296,7 +296,9 @@ pub(super) fn require( .copied() .collect::>(); - let to_print = if ns.target == Target::Soroban { + let to_print = if ns.target == Target::Soroban || ns.target == Target::Antelope { + // Soroban/Antelope: use a static string to avoid heap allocation + // (FormatString requires vector_new → __malloc → heap) Expression::BytesLiteral { loc: Codegen, ty: Type::String, diff --git a/src/emit/antelope/mod.rs b/src/emit/antelope/mod.rs new file mode 100644 index 000000000..094dcc297 --- /dev/null +++ b/src/emit/antelope/mod.rs @@ -0,0 +1,1573 @@ +// SPDX-License-Identifier: Apache-2.0 + +pub(super) mod target; + +use crate::codegen::Options; +use crate::emit::binary::Binary; +use crate::emit::cfg::emit_cfg; +use crate::sema::ast; +use crate::sema::ast::Type; +use inkwell::context::Context; +use inkwell::module::{Linkage, Module}; +use inkwell::values::{BasicMetadataValueEnum, GlobalValue, IntValue}; +use inkwell::AddressSpace; +use inkwell::IntPredicate; + +pub struct AntelopeTarget; + +/// Encode a string as an Antelope eosio::name uint64. +/// Characters: '.' = 0, '1'-'5' = 1-5, 'a'-'z' = 6-31 +/// First 12 chars use 5 bits each (bits 59..0), 13th char uses 4 bits. +pub fn string_to_name(s: &str) -> u64 { + let char_to_value = |c: u8| -> u64 { + match c { + b'.' => 0, + b'1'..=b'5' => (c - b'1' + 1) as u64, + b'a'..=b'z' => (c - b'a' + 6) as u64, + _ => 0, + } + }; + + let bytes = s.as_bytes(); + let len = bytes.len().min(13); + let mut value: u64 = 0; + + for i in 0..len.min(12) { + value |= char_to_value(bytes[i]) << (64 - 5 * (i + 1)); + } + if len == 13 { + value |= char_to_value(bytes[12]) & 0x0F; + } + + value +} + +/// The table name used for all Solidity state variable storage. +/// Encoded as eosio::name("state"). +pub const STATE_TABLE_NAME: u64 = { + // "state" = s(24) t(25) a(6) t(25) e(10) + // = (24<<59) | (25<<54) | (6<<49) | (25<<44) | (10<<39) + (24 << 59) | (25 << 54) | (6 << 49) | (25 << 44) | (10 << 39) +}; + +impl AntelopeTarget { + pub fn build<'a>( + context: &'a Context, + std_lib: &Module<'a>, + contract: &'a ast::Contract, + ns: &'a ast::Namespace, + opt: &'a Options, + ) -> Binary<'a> { + let filename = ns.files[contract.loc.file_no()].file_name(); + let mut bin = Binary::new( + context, + ns, + &contract.id.name, + &filename, + opt, + std_lib, + None, + ); + + let mut export_list = Vec::new(); + + Self::declare_externals(&mut bin); + Self::add_receiver_global(&mut bin); + Self::emit_varuint32_helpers(context, &mut bin); + Self::emit_storage_helpers(context, &mut bin); + Self::emit_functions(contract, &mut bin); + Self::emit_apply(context, &mut bin, contract, &mut export_list); + + bin.internalize(export_list.as_slice()); + + // The bundled allocator comes from the Soroban stdlib, where each function carries a + // `wasm-export-name` attribute because the Stellar host calls the guest allocator by + // name. The Antelope host only ever calls `apply`, so those exports are useless here — + // and being exported pins them as wasm-opt GC roots, preventing dead-code elimination + // of the ones the contract never uses. Strip the export attribute (apply is exported + // via linkage, not this attribute, so it is unaffected) and let DCE remove the rest. + let mut f = bin.module.get_first_function(); + while let Some(func) = f { + func.remove_string_attribute( + inkwell::attributes::AttributeLoc::Function, + "wasm-export-name", + ); + f = func.get_next_function(); + } + + bin + } + + /// Declare Antelope host function imports. + fn declare_externals(bin: &mut Binary) { + let i32_ty = bin.context.i32_type(); + let i64_ty = bin.context.i64_type(); + let void_ty = bin.context.void_type(); + let ptr_ty = bin.context.ptr_type(AddressSpace::default()); + + // void prints_l(const char* msg, uint32_t len) + let prints_l_ty = void_ty.fn_type(&[ptr_ty.into(), i32_ty.into()], false); + bin.module + .add_function("prints_l", prints_l_ty, Some(Linkage::External)); + + // void eosio_assert(uint32_t test, const char* msg) + let eosio_assert_ty = void_ty.fn_type(&[i32_ty.into(), ptr_ty.into()], false); + bin.module + .add_function("eosio_assert", eosio_assert_ty, Some(Linkage::External)); + + // int32_t db_find_i64(uint64_t code, uint64_t scope, uint64_t table, uint64_t id) + let db_find_ty = i32_ty.fn_type( + &[i64_ty.into(), i64_ty.into(), i64_ty.into(), i64_ty.into()], + false, + ); + bin.module + .add_function("db_find_i64", db_find_ty, Some(Linkage::External)); + + // int32_t db_store_i64(uint64_t scope, uint64_t table, uint64_t payer, uint64_t id, const void* data, uint32_t len) + let db_store_ty = i32_ty.fn_type( + &[ + i64_ty.into(), + i64_ty.into(), + i64_ty.into(), + i64_ty.into(), + ptr_ty.into(), + i32_ty.into(), + ], + false, + ); + bin.module + .add_function("db_store_i64", db_store_ty, Some(Linkage::External)); + + // void db_update_i64(int32_t iterator, uint64_t payer, const void* data, uint32_t len) + let db_update_ty = void_ty.fn_type( + &[i32_ty.into(), i64_ty.into(), ptr_ty.into(), i32_ty.into()], + false, + ); + bin.module + .add_function("db_update_i64", db_update_ty, Some(Linkage::External)); + + // int32_t db_get_i64(int32_t iterator, void* data, uint32_t len) + let db_get_ty = + i32_ty.fn_type(&[i32_ty.into(), ptr_ty.into(), i32_ty.into()], false); + bin.module + .add_function("db_get_i64", db_get_ty, Some(Linkage::External)); + + // uint32_t action_data_size() + let action_data_size_ty = i32_ty.fn_type(&[], false); + bin.module.add_function( + "action_data_size", + action_data_size_ty, + Some(Linkage::External), + ); + + // uint32_t read_action_data(void* msg, uint32_t len) + let read_action_data_ty = i32_ty.fn_type(&[ptr_ty.into(), i32_ty.into()], false); + bin.module.add_function( + "read_action_data", + read_action_data_ty, + Some(Linkage::External), + ); + + // void sha3(const char* data, uint32_t data_len, char* hash, uint32_t hash_len, int32_t keccak) + // keccak=1 for keccak256 mode (Ethereum-compatible) + let sha3_ty = void_ty.fn_type( + &[ptr_ty.into(), i32_ty.into(), ptr_ty.into(), i32_ty.into(), i32_ty.into()], + false, + ); + bin.module + .add_function("sha3", sha3_ty, Some(Linkage::External)); + + // int32_t db_end_i64(uint64_t code, uint64_t scope, uint64_t table) + let db_end_ty = i32_ty.fn_type( + &[i64_ty.into(), i64_ty.into(), i64_ty.into()], + false, + ); + bin.module + .add_function("db_end_i64", db_end_ty, Some(Linkage::External)); + + // int32_t db_previous_i64(int32_t iterator, uint64_t* primary) + let db_previous_ty = i32_ty.fn_type(&[i32_ty.into(), ptr_ty.into()], false); + bin.module + .add_function("db_previous_i64", db_previous_ty, Some(Linkage::External)); + + // int32_t db_idx256_store(uint64_t scope, uint64_t table, uint64_t payer, uint64_t id, const uint128_t data[], uint32_t data_len) + let db_idx256_store_ty = i32_ty.fn_type( + &[ + i64_ty.into(), + i64_ty.into(), + i64_ty.into(), + i64_ty.into(), + ptr_ty.into(), + i32_ty.into(), + ], + false, + ); + bin.module.add_function( + "db_idx256_store", + db_idx256_store_ty, + Some(Linkage::External), + ); + + // int32_t db_idx256_find_secondary(uint64_t code, uint64_t scope, uint64_t table, const uint128_t data[], uint32_t data_len, uint64_t* primary) + let db_idx256_find_ty = i32_ty.fn_type( + &[ + i64_ty.into(), + i64_ty.into(), + i64_ty.into(), + ptr_ty.into(), + i32_ty.into(), + ptr_ty.into(), + ], + false, + ); + bin.module.add_function( + "db_idx256_find_secondary", + db_idx256_find_ty, + Some(Linkage::External), + ); + + // void db_idx256_update(int32_t iterator, uint64_t payer, const uint128_t data[], uint32_t data_len) + let db_idx256_update_ty = void_ty.fn_type( + &[i32_ty.into(), i64_ty.into(), ptr_ty.into(), i32_ty.into()], + false, + ); + bin.module.add_function( + "db_idx256_update", + db_idx256_update_ty, + Some(Linkage::External), + ); + + // void db_remove_i64(int32_t iterator) + let db_remove_ty = void_ty.fn_type(&[i32_ty.into()], false); + bin.module + .add_function("db_remove_i64", db_remove_ty, Some(Linkage::External)); + + // void db_idx256_remove(int32_t iterator) + let db_idx256_remove_ty = void_ty.fn_type(&[i32_ty.into()], false); + bin.module.add_function( + "db_idx256_remove", + db_idx256_remove_ty, + Some(Linkage::External), + ); + + // void require_auth(uint64_t name) + let require_auth_ty = void_ty.fn_type(&[i64_ty.into()], false); + bin.module + .add_function("require_auth", require_auth_ty, Some(Linkage::External)); + + // bool has_auth(uint64_t name) → returns i32 (C bool) + let has_auth_ty = i32_ty.fn_type(&[i64_ty.into()], false); + bin.module + .add_function("has_auth", has_auth_ty, Some(Linkage::External)); + + // void require_auth2(uint64_t account, uint64_t permission) + let require_auth2_ty = void_ty.fn_type(&[i64_ty.into(), i64_ty.into()], false); + bin.module + .add_function("require_auth2", require_auth2_ty, Some(Linkage::External)); + + // uint64_t current_time() + let current_time_ty = i64_ty.fn_type(&[], false); + bin.module + .add_function("current_time", current_time_ty, Some(Linkage::External)); + + // uint64_t current_receiver() + let current_receiver_ty = i64_ty.fn_type(&[], false); + bin.module.add_function( + "current_receiver", + current_receiver_ty, + Some(Linkage::External), + ); + + // void require_recipient(uint64_t name) + let require_recipient_ty = void_ty.fn_type(&[i64_ty.into()], false); + bin.module.add_function( + "require_recipient", + require_recipient_ty, + Some(Linkage::External), + ); + + // void send_inline(const char* serialized_action, uint32_t size) + let send_inline_ty = void_ty.fn_type(&[ptr_ty.into(), i32_ty.into()], false); + bin.module + .add_function("send_inline", send_inline_ty, Some(Linkage::External)); + + // --- Table read host functions --- + + // int32_t db_next_i64(int32_t iterator, uint64_t* primary) + let db_next_ty = i32_ty.fn_type(&[i32_ty.into(), ptr_ty.into()], false); + bin.module + .add_function("db_next_i64", db_next_ty, Some(Linkage::External)); + + // int32_t db_lowerbound_i64(uint64_t code, uint64_t scope, uint64_t table, uint64_t id) + let db_lowerbound_ty = i32_ty.fn_type( + &[i64_ty.into(), i64_ty.into(), i64_ty.into(), i64_ty.into()], + false, + ); + bin.module + .add_function("db_lowerbound_i64", db_lowerbound_ty, Some(Linkage::External)); + + // int32_t db_idx64_find_secondary(uint64_t code, uint64_t scope, uint64_t table, const uint64_t* secondary, uint64_t* primary) + let db_idx64_find_ty = i32_ty.fn_type( + &[i64_ty.into(), i64_ty.into(), i64_ty.into(), ptr_ty.into(), ptr_ty.into()], + false, + ); + bin.module + .add_function("db_idx64_find_secondary", db_idx64_find_ty, Some(Linkage::External)); + + // int32_t db_idx64_lowerbound(uint64_t code, uint64_t scope, uint64_t table, uint64_t* secondary, uint64_t* primary) + let db_idx64_lb_ty = i32_ty.fn_type( + &[i64_ty.into(), i64_ty.into(), i64_ty.into(), ptr_ty.into(), ptr_ty.into()], + false, + ); + bin.module + .add_function("db_idx64_lowerbound", db_idx64_lb_ty, Some(Linkage::External)); + + // int32_t db_idx128_find_secondary(uint64_t code, uint64_t scope, uint64_t table, const uint128_t* secondary, uint64_t* primary) + let db_idx128_find_ty = i32_ty.fn_type( + &[i64_ty.into(), i64_ty.into(), i64_ty.into(), ptr_ty.into(), ptr_ty.into()], + false, + ); + bin.module + .add_function("db_idx128_find_secondary", db_idx128_find_ty, Some(Linkage::External)); + + // int32_t db_idx128_lowerbound(uint64_t code, uint64_t scope, uint64_t table, uint128_t* secondary, uint64_t* primary) + let db_idx128_lb_ty = i32_ty.fn_type( + &[i64_ty.into(), i64_ty.into(), i64_ty.into(), ptr_ty.into(), ptr_ty.into()], + false, + ); + bin.module + .add_function("db_idx128_lowerbound", db_idx128_lb_ty, Some(Linkage::External)); + + // int32_t db_idx256_lowerbound(uint64_t code, uint64_t scope, uint64_t table, const uint128_t data[], uint32_t data_len, uint64_t* primary) + let db_idx256_lb_ty = i32_ty.fn_type( + &[i64_ty.into(), i64_ty.into(), i64_ty.into(), ptr_ty.into(), i32_ty.into(), ptr_ty.into()], + false, + ); + bin.module + .add_function("db_idx256_lowerbound", db_idx256_lb_ty, Some(Linkage::External)); + + // void set_action_return_value(const void* data, uint32_t len) + let set_action_ret_ty = void_ty.fn_type(&[ptr_ty.into(), i32_ty.into()], false); + bin.module + .add_function("set_action_return_value", set_action_ret_ty, Some(Linkage::External)); + } + + /// Add WASM globals for receiver and auto-increment pk cache. + fn add_receiver_global(bin: &mut Binary) { + let i64_ty = bin.context.i64_type(); + + // __receiver: stores the current contract's account name (set in apply()). + let global = bin.module.add_global(i64_ty, None, "__receiver"); + global.set_initializer(&i64_ty.const_zero()); + global.set_linkage(Linkage::Internal); + + // __next_pk: cached next primary key for auto-increment storage inserts. + // Initialized to UINT64_MAX as sentinel meaning "not yet computed". + // On first insert, computed via db_end_i64/db_previous_i64, then incremented. + let pk_global = bin.module.add_global(i64_ty, None, "__next_pk"); + pk_global.set_initializer(&i64_ty.const_all_ones()); // UINT64_MAX sentinel + pk_global.set_linkage(Linkage::Internal); + + // __ram_payer: RAM payer for storage operations. + // 0 = use receiver (default). Set via antelope.setpayer(account). + let payer_global = bin.module.add_global(i64_ty, None, "__ram_payer"); + payer_global.set_initializer(&i64_ty.const_zero()); + payer_global.set_linkage(Linkage::Internal); + + // __code: the account that sent this action (set in apply()). + // Equals receiver on direct calls; differs on notifications (require_recipient). + let code_global = bin.module.add_global(i64_ty, None, "__code"); + code_global.set_initializer(&i64_ty.const_zero()); + code_global.set_linkage(Linkage::Internal); + + // __last_pk: cached primary key from the last dbNext/dbIdx*Find/dbIdx*Lowerbound call. + // Read via antelope.lastPk(). Initialized to 0. + let last_pk_global = bin.module.add_global(i64_ty, None, "__last_pk"); + last_pk_global.set_initializer(&i64_ty.const_zero()); + last_pk_global.set_linkage(Linkage::Internal); + } + + /// Get the __receiver global value. + pub fn get_receiver_global<'a>(bin: &Binary<'a>) -> GlobalValue<'a> { + bin.module.get_global("__receiver").unwrap() + } + + /// Get the __next_pk global value. + pub fn get_next_pk_global<'a>(bin: &Binary<'a>) -> GlobalValue<'a> { + bin.module.get_global("__next_pk").unwrap() + } + + /// Get the RAM payer: if __ram_payer != 0, use it; otherwise use receiver. + pub fn get_ram_payer<'a>(bin: &Binary<'a>) -> IntValue<'a> { + let i64_ty = bin.context.i64_type(); + let payer_global = bin.module.get_global("__ram_payer").unwrap(); + let payer = bin + .builder + .build_load(i64_ty, payer_global.as_pointer_value(), "payer") + .unwrap() + .into_int_value(); + + let receiver_global = bin.module.get_global("__receiver").unwrap(); + let receiver = bin + .builder + .build_load(i64_ty, receiver_global.as_pointer_value(), "recv") + .unwrap() + .into_int_value(); + + // if payer != 0 { payer } else { receiver } + let is_set = bin + .builder + .build_int_compare(inkwell::IntPredicate::NE, payer, i64_ty.const_zero(), "pset") + .unwrap(); + bin.builder + .build_select(is_set, payer, receiver, "ram_payer") + .unwrap() + .into_int_value() + } + + /// Emit helper functions for varuint32 encoding/decoding. + /// + /// __encode_varuint32(buf: *mut u8, value: u32) -> u32 (bytes written) + /// __decode_varuint32(buf: *const u8) -> u64 (low 32 bits = value, high 32 bits = bytes read) + fn emit_varuint32_helpers<'a>(context: &'a Context, bin: &mut Binary<'a>) { + let i8_ty = context.i8_type(); + let i32_ty = context.i32_type(); + let i64_ty = context.i64_type(); + let ptr_ty = context.ptr_type(inkwell::AddressSpace::default()); + + // --- __encode_varuint32(buf, value) -> bytes_written --- + { + let fn_ty = i32_ty.fn_type(&[ptr_ty.into(), i32_ty.into()], false); + let func = bin + .module + .add_function("__encode_varuint32", fn_ty, Some(Linkage::Internal)); + + let entry = context.append_basic_block(func, "entry"); + let loop_bb = context.append_basic_block(func, "loop"); + let done_bb = context.append_basic_block(func, "done"); + + bin.builder.position_at_end(entry); + let buf = func.get_nth_param(0).unwrap().into_pointer_value(); + let value = func.get_nth_param(1).unwrap().into_int_value(); + + // offset = 0, remaining = value + let offset_alloca = bin.builder.build_alloca(i32_ty, "offset").unwrap(); + let remain_alloca = bin.builder.build_alloca(i32_ty, "remain").unwrap(); + bin.builder + .build_store(offset_alloca, i32_ty.const_zero()) + .unwrap(); + bin.builder.build_store(remain_alloca, value).unwrap(); + bin.builder.build_unconditional_branch(loop_bb).unwrap(); + + // Loop: write one byte at a time + bin.builder.position_at_end(loop_bb); + let remain = bin + .builder + .build_load(i32_ty, remain_alloca, "rem") + .unwrap() + .into_int_value(); + let offset = bin + .builder + .build_load(i32_ty, offset_alloca, "off") + .unwrap() + .into_int_value(); + + // byte = remain & 0x7F + let byte_val = bin + .builder + .build_and(remain, i32_ty.const_int(0x7F, false), "byte") + .unwrap(); + // remain >>= 7 + let new_remain = bin + .builder + .build_right_shift(remain, i32_ty.const_int(7, false), false, "shr") + .unwrap(); + + // if new_remain > 0: set high bit + let has_more = bin + .builder + .build_int_compare(IntPredicate::UGT, new_remain, i32_ty.const_zero(), "more") + .unwrap(); + let high_bit = bin + .builder + .build_select(has_more, i32_ty.const_int(0x80, false), i32_ty.const_zero(), "hb") + .unwrap() + .into_int_value(); + let final_byte = bin.builder.build_or(byte_val, high_bit, "fb").unwrap(); + + // buf[offset] = final_byte + let byte_ptr = unsafe { + bin.builder + .build_gep(i8_ty, buf, &[offset], "bp") + .unwrap() + }; + let byte_i8 = bin + .builder + .build_int_truncate(final_byte, i8_ty, "b8") + .unwrap(); + bin.builder.build_store(byte_ptr, byte_i8).unwrap(); + + // offset++ + let new_offset = bin + .builder + .build_int_add(offset, i32_ty.const_int(1, false), "no") + .unwrap(); + bin.builder + .build_store(offset_alloca, new_offset) + .unwrap(); + bin.builder + .build_store(remain_alloca, new_remain) + .unwrap(); + + bin.builder + .build_conditional_branch(has_more, loop_bb, done_bb) + .unwrap(); + + bin.builder.position_at_end(done_bb); + let final_offset = bin + .builder + .build_load(i32_ty, offset_alloca, "final_off") + .unwrap(); + bin.builder.build_return(Some(&final_offset)).unwrap(); + } + + // --- __decode_varuint32(buf) -> u64 (low32=value, high32=bytes_read) --- + { + let fn_ty = i64_ty.fn_type(&[ptr_ty.into()], false); + let func = bin + .module + .add_function("__decode_varuint32", fn_ty, Some(Linkage::Internal)); + + let entry = context.append_basic_block(func, "entry"); + let loop_bb = context.append_basic_block(func, "loop"); + let done_bb = context.append_basic_block(func, "done"); + + bin.builder.position_at_end(entry); + let buf = func.get_nth_param(0).unwrap().into_pointer_value(); + + let result_alloca = bin.builder.build_alloca(i32_ty, "result").unwrap(); + let shift_alloca = bin.builder.build_alloca(i32_ty, "shift").unwrap(); + let offset_alloca = bin.builder.build_alloca(i32_ty, "offset").unwrap(); + + bin.builder + .build_store(result_alloca, i32_ty.const_zero()) + .unwrap(); + bin.builder + .build_store(shift_alloca, i32_ty.const_zero()) + .unwrap(); + bin.builder + .build_store(offset_alloca, i32_ty.const_zero()) + .unwrap(); + bin.builder.build_unconditional_branch(loop_bb).unwrap(); + + bin.builder.position_at_end(loop_bb); + let offset = bin + .builder + .build_load(i32_ty, offset_alloca, "off") + .unwrap() + .into_int_value(); + let shift = bin + .builder + .build_load(i32_ty, shift_alloca, "sh") + .unwrap() + .into_int_value(); + let result = bin + .builder + .build_load(i32_ty, result_alloca, "res") + .unwrap() + .into_int_value(); + + // byte = buf[offset] + let byte_ptr = unsafe { + bin.builder + .build_gep(i8_ty, buf, &[offset], "bp") + .unwrap() + }; + let byte_val = bin + .builder + .build_load(i8_ty, byte_ptr, "bv") + .unwrap() + .into_int_value(); + let byte_i32 = bin + .builder + .build_int_z_extend(byte_val, i32_ty, "b32") + .unwrap(); + + // result |= (byte & 0x7F) << shift + let masked = bin + .builder + .build_and(byte_i32, i32_ty.const_int(0x7F, false), "m") + .unwrap(); + let shifted = bin.builder.build_left_shift(masked, shift, "sl").unwrap(); + let new_result = bin.builder.build_or(result, shifted, "nr").unwrap(); + bin.builder + .build_store(result_alloca, new_result) + .unwrap(); + + // shift += 7 + let new_shift = bin + .builder + .build_int_add(shift, i32_ty.const_int(7, false), "ns") + .unwrap(); + bin.builder + .build_store(shift_alloca, new_shift) + .unwrap(); + + // offset++ + let new_offset = bin + .builder + .build_int_add(offset, i32_ty.const_int(1, false), "no") + .unwrap(); + bin.builder + .build_store(offset_alloca, new_offset) + .unwrap(); + + // if byte & 0x80: continue + let has_more = bin + .builder + .build_int_compare( + IntPredicate::NE, + bin.builder + .build_and(byte_i32, i32_ty.const_int(0x80, false), "hb") + .unwrap(), + i32_ty.const_zero(), + "more", + ) + .unwrap(); + bin.builder + .build_conditional_branch(has_more, loop_bb, done_bb) + .unwrap(); + + // Done: pack (value, bytes_read) into i64 + bin.builder.position_at_end(done_bb); + let final_result = bin + .builder + .build_load(i32_ty, result_alloca, "fv") + .unwrap() + .into_int_value(); + let final_offset = bin + .builder + .build_load(i32_ty, offset_alloca, "fo") + .unwrap() + .into_int_value(); + + // packed = (bytes_read << 32) | value + let result_i64 = bin + .builder + .build_int_z_extend(final_result, i64_ty, "r64") + .unwrap(); + let offset_i64 = bin + .builder + .build_int_z_extend(final_offset, i64_ty, "o64") + .unwrap(); + let shifted_offset = bin + .builder + .build_left_shift(offset_i64, i64_ty.const_int(32, false), "so") + .unwrap(); + let packed = bin + .builder + .build_or(result_i64, shifted_offset, "packed") + .unwrap(); + bin.builder.build_return(Some(&packed)).unwrap(); + } + } + + /// Emit the shared storage read-modify-write helpers, once per module. + /// + /// Every scalar storage access used to be fully inlined (slot buffer + row buffer + + /// idx256 find + update-or-insert with the cached __next_pk allocation), which made + /// storage-heavy functions huge. These two internal functions hold that sequence once; + /// `storage_store`/`storage_load` now just call them with (slot, value-bytes, len). + /// They are byte-oriented (value passed via pointer + length) so they're type-agnostic. + /// + /// void __antelope_store_slot(i256 slot, i8* val_ptr, i32 val_len) + /// void __antelope_load_slot (i256 slot, i8* out_ptr, i32 val_len) // writes out_ptr only on hit + fn emit_storage_helpers<'a>(context: &'a Context, bin: &mut Binary<'a>) { + let i8_ty = context.i8_type(); + let i32_ty = context.i32_type(); + let i64_ty = context.i64_type(); + let i256_ty = context.custom_width_int_type(256); + let ptr_ty = context.ptr_type(AddressSpace::default()); + let void_ty = context.void_type(); + let table_name = i64_ty.const_int(STATE_TABLE_NAME, false); + let data_len = i32_ty.const_int(2, false); // idx256 key = 2 x uint128_t = 256 bits + + // Keep the helpers outlined: without this the optimizer inlines the smaller one + // back into every call site, defeating the size win. + let noinline = context.create_enum_attribute( + inkwell::attributes::Attribute::get_named_enum_kind_id("noinline"), + 0, + ); + // __map_slot is a pure function of its arguments (deterministic hash; the only + // memory it touches is non-escaping local allocas). Marking it readnone lets the + // EarlyCSE pass merge repeated identical slot derivations (e.g. the load and store + // of `m[k] += v`, or `m[k].a`/`m[k].b`). Safe: the result depends only on the args. + let readnone = context.create_enum_attribute( + inkwell::attributes::Attribute::get_named_enum_kind_id("readnone"), + 0, + ); + + // ── void __antelope_store_slot(i256 slot, i8* val_ptr, i32 val_len) ── + { + let fn_ty = + void_ty.fn_type(&[i256_ty.into(), ptr_ty.into(), i32_ty.into()], false); + let func = bin + .module + .add_function("__antelope_store_slot", fn_ty, Some(Linkage::Internal)); + func.add_attribute(inkwell::attributes::AttributeLoc::Function, noinline); + let entry = context.append_basic_block(func, "entry"); + bin.builder.position_at_end(entry); + + let slot = func.get_nth_param(0).unwrap().into_int_value(); + let val_ptr = func.get_nth_param(1).unwrap().into_pointer_value(); + let val_len = func.get_nth_param(2).unwrap().into_int_value(); + + let receiver = bin + .builder + .build_load( + i64_ty, + Self::get_receiver_global(bin).as_pointer_value(), + "receiver", + ) + .unwrap() + .into_int_value(); + let ram_payer = Self::get_ram_payer(bin); + + // slot_buf[32] = slot + let slot_buf = bin + .builder + .build_array_alloca(i8_ty, i32_ty.const_int(32, false), "slot_buf") + .unwrap(); + bin.builder.build_store(slot_buf, slot).unwrap(); + + // row_buf = [pk(8) | slot_hash(32) | varuint32(1) | value(val_len)] + let row_size = bin + .builder + .build_int_add(i32_ty.const_int(41, false), val_len, "row_size") + .unwrap(); + let row_buf = bin + .builder + .build_array_alloca(i8_ty, row_size, "row_buf") + .unwrap(); + // slot_hash at offset 8 + let hash_ptr = unsafe { + bin.builder + .build_gep(i8_ty, row_buf, &[i32_ty.const_int(8, false)], "hash_ptr") + .unwrap() + }; + bin.builder.build_store(hash_ptr, slot).unwrap(); + // varuint32 length at offset 40 (1 byte; callers pass val_len <= 127) + let len_ptr = unsafe { + bin.builder + .build_gep(i8_ty, row_buf, &[i32_ty.const_int(40, false)], "len_ptr") + .unwrap() + }; + let len_i8 = bin.builder.build_int_truncate(val_len, i8_ty, "len8").unwrap(); + bin.builder.build_store(len_ptr, len_i8).unwrap(); + // value at offset 41 + let dst_val = unsafe { + bin.builder + .build_gep(i8_ty, row_buf, &[i32_ty.const_int(41, false)], "dst_val") + .unwrap() + }; + let memcpy = bin.module.get_function("__memcpy").unwrap(); + bin.builder + .build_call(memcpy, &[dst_val.into(), val_ptr.into(), val_len.into()], "") + .unwrap(); + + // Look up via idx256 secondary index. + let pk_out = bin.builder.build_alloca(i64_ty, "pk_out").unwrap(); + let db_idx256_find = bin.module.get_function("db_idx256_find_secondary").unwrap(); + let sec_iter = bin + .builder + .build_call( + db_idx256_find, + &[ + receiver.into(), + receiver.into(), + table_name.into(), + slot_buf.into(), + data_len.into(), + pk_out.into(), + ], + "sec_iter", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + let found = bin + .builder + .build_int_compare(IntPredicate::SGE, sec_iter, i32_ty.const_zero(), "found") + .unwrap(); + + let update_bb = context.append_basic_block(func, "idx_update"); + let insert_bb = context.append_basic_block(func, "idx_insert"); + let done_bb = context.append_basic_block(func, "idx_done"); + bin.builder + .build_conditional_branch(found, update_bb, insert_bb) + .unwrap(); + + // UPDATE existing row. + bin.builder.position_at_end(update_bb); + let pk = bin.builder.build_load(i64_ty, pk_out, "pk").unwrap().into_int_value(); + bin.builder.build_store(row_buf, pk).unwrap(); + let db_find = bin.module.get_function("db_find_i64").unwrap(); + let pri_iter = bin + .builder + .build_call( + db_find, + &[receiver.into(), receiver.into(), table_name.into(), pk.into()], + "pri_iter", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + let db_update = bin.module.get_function("db_update_i64").unwrap(); + bin.builder + .build_call( + db_update, + &[pri_iter.into(), ram_payer.into(), row_buf.into(), row_size.into()], + "", + ) + .unwrap(); + bin.builder.build_unconditional_branch(done_bb).unwrap(); + + // INSERT new row with auto-increment primary key (cached __next_pk; + // UINT64_MAX sentinel means "compute from DB via db_end/db_previous"). + bin.builder.position_at_end(insert_bb); + let pk_global = Self::get_next_pk_global(bin); + let cached_pk = bin + .builder + .build_load(i64_ty, pk_global.as_pointer_value(), "cached_pk") + .unwrap() + .into_int_value(); + let sentinel = i64_ty.const_all_ones(); + let need_init = bin + .builder + .build_int_compare(IntPredicate::EQ, cached_pk, sentinel, "need_init") + .unwrap(); + let init_bb = context.append_basic_block(func, "pk_init"); + let use_cached_bb = context.append_basic_block(func, "pk_cached"); + let do_insert_bb = context.append_basic_block(func, "do_insert"); + bin.builder + .build_conditional_branch(need_init, init_bb, use_cached_bb) + .unwrap(); + + // INIT: compute next pk from the table. + bin.builder.position_at_end(init_bb); + let db_end = bin.module.get_function("db_end_i64").unwrap(); + let end_iter = bin + .builder + .build_call( + db_end, + &[receiver.into(), receiver.into(), table_name.into()], + "end_iter", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + let end_neg = bin + .builder + .build_int_compare( + IntPredicate::EQ, + end_iter, + i32_ty.const_int(u64::MAX, true), // -1 (empty table) + "end_neg", + ) + .unwrap(); + let empty_bb = context.append_basic_block(func, "table_empty"); + let has_rows_bb = context.append_basic_block(func, "table_has_rows"); + let init_done_bb = context.append_basic_block(func, "pk_init_done"); + bin.builder + .build_conditional_branch(end_neg, empty_bb, has_rows_bb) + .unwrap(); + + // Empty table → pk 0. + bin.builder.position_at_end(empty_bb); + let pk_zero = i64_ty.const_zero(); + bin.builder.build_unconditional_branch(init_done_bb).unwrap(); + + // Has rows → last pk + 1. + bin.builder.position_at_end(has_rows_bb); + let last_pk_out = bin.builder.build_alloca(i64_ty, "last_pk_out").unwrap(); + let db_previous = bin.module.get_function("db_previous_i64").unwrap(); + bin.builder + .build_call(db_previous, &[end_iter.into(), last_pk_out.into()], "") + .unwrap(); + let last_pk = bin + .builder + .build_load(i64_ty, last_pk_out, "last_pk") + .unwrap() + .into_int_value(); + let pk_from_db = bin + .builder + .build_int_add(last_pk, i64_ty.const_int(1, false), "pk_from_db") + .unwrap(); + bin.builder.build_unconditional_branch(init_done_bb).unwrap(); + + bin.builder.position_at_end(init_done_bb); + let init_pk = bin.builder.build_phi(i64_ty, "init_pk").unwrap(); + init_pk.add_incoming(&[(&pk_zero, empty_bb), (&pk_from_db, has_rows_bb)]); + let init_pk_val = init_pk.as_basic_value().into_int_value(); + bin.builder.build_unconditional_branch(do_insert_bb).unwrap(); + + bin.builder.position_at_end(use_cached_bb); + bin.builder.build_unconditional_branch(do_insert_bb).unwrap(); + + // Insert with the chosen pk, then bump __next_pk. + bin.builder.position_at_end(do_insert_bb); + let new_pk = bin.builder.build_phi(i64_ty, "new_pk").unwrap(); + new_pk.add_incoming(&[(&init_pk_val, init_done_bb), (&cached_pk, use_cached_bb)]); + let new_pk_val = new_pk.as_basic_value().into_int_value(); + bin.builder.build_store(row_buf, new_pk_val).unwrap(); + let next_pk_inc = bin + .builder + .build_int_add(new_pk_val, i64_ty.const_int(1, false), "next_pk_inc") + .unwrap(); + bin.builder + .build_store(pk_global.as_pointer_value(), next_pk_inc) + .unwrap(); + let db_store = bin.module.get_function("db_store_i64").unwrap(); + bin.builder + .build_call( + db_store, + &[ + receiver.into(), + table_name.into(), + ram_payer.into(), + new_pk_val.into(), + row_buf.into(), + row_size.into(), + ], + "", + ) + .unwrap(); + let db_idx256_store = bin.module.get_function("db_idx256_store").unwrap(); + bin.builder + .build_call( + db_idx256_store, + &[ + receiver.into(), + table_name.into(), + ram_payer.into(), + new_pk_val.into(), + slot_buf.into(), + data_len.into(), + ], + "", + ) + .unwrap(); + bin.builder.build_unconditional_branch(done_bb).unwrap(); + + bin.builder.position_at_end(done_bb); + bin.builder.build_return(None).unwrap(); + } + + // ── void __antelope_load_slot(i256 slot, i8* out_ptr, i32 val_len) ── + // Writes out_ptr only on a hit; callers pre-zero the buffer so a miss reads as 0. + { + let fn_ty = + void_ty.fn_type(&[i256_ty.into(), ptr_ty.into(), i32_ty.into()], false); + let func = bin + .module + .add_function("__antelope_load_slot", fn_ty, Some(Linkage::Internal)); + func.add_attribute(inkwell::attributes::AttributeLoc::Function, noinline); + let entry = context.append_basic_block(func, "entry"); + bin.builder.position_at_end(entry); + + let slot = func.get_nth_param(0).unwrap().into_int_value(); + let out_ptr = func.get_nth_param(1).unwrap().into_pointer_value(); + let val_len = func.get_nth_param(2).unwrap().into_int_value(); + + let receiver = bin + .builder + .build_load( + i64_ty, + Self::get_receiver_global(bin).as_pointer_value(), + "receiver", + ) + .unwrap() + .into_int_value(); + + let slot_buf = bin + .builder + .build_array_alloca(i8_ty, i32_ty.const_int(32, false), "slot_buf") + .unwrap(); + bin.builder.build_store(slot_buf, slot).unwrap(); + + let pk_out = bin.builder.build_alloca(i64_ty, "pk_out").unwrap(); + let db_idx256_find = bin.module.get_function("db_idx256_find_secondary").unwrap(); + let sec_iter = bin + .builder + .build_call( + db_idx256_find, + &[ + receiver.into(), + receiver.into(), + table_name.into(), + slot_buf.into(), + data_len.into(), + pk_out.into(), + ], + "sec_iter", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + let found = bin + .builder + .build_int_compare(IntPredicate::SGE, sec_iter, i32_ty.const_zero(), "found") + .unwrap(); + + let found_bb = context.append_basic_block(func, "idx_found"); + let done_bb = context.append_basic_block(func, "idx_done"); + bin.builder + .build_conditional_branch(found, found_bb, done_bb) + .unwrap(); + + bin.builder.position_at_end(found_bb); + let pk = bin.builder.build_load(i64_ty, pk_out, "pk").unwrap().into_int_value(); + let db_find = bin.module.get_function("db_find_i64").unwrap(); + let pri_iter = bin + .builder + .build_call( + db_find, + &[receiver.into(), receiver.into(), table_name.into(), pk.into()], + "pri_iter", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + let row_size = bin + .builder + .build_int_add(i32_ty.const_int(41, false), val_len, "row_size") + .unwrap(); + let row_buf = bin + .builder + .build_array_alloca(i8_ty, row_size, "row_buf") + .unwrap(); + let db_get = bin.module.get_function("db_get_i64").unwrap(); + bin.builder + .build_call(db_get, &[pri_iter.into(), row_buf.into(), row_size.into()], "") + .unwrap(); + let src_val = unsafe { + bin.builder + .build_gep(i8_ty, row_buf, &[i32_ty.const_int(41, false)], "src_val") + .unwrap() + }; + let memcpy = bin.module.get_function("__memcpy").unwrap(); + bin.builder + .build_call(memcpy, &[out_ptr.into(), src_val.into(), val_len.into()], "") + .unwrap(); + bin.builder.build_unconditional_branch(done_bb).unwrap(); + + bin.builder.position_at_end(done_bb); + bin.builder.build_return(None).unwrap(); + } + + // ── i256 __map_slot(i256 prev, i256 key, i32 key_len) ── + // slot_hash = keccak256( prev (32 bytes, LE) ‖ key (low key_len bytes, LE) ). + // One helper covers every fixed-size mapping key (<= 256 bits): the caller + // zero-extends the key to 256 bits and passes its byte length, and only the + // first (32 + key_len) bytes are hashed — so the preimage is byte-identical to + // the old inline build. This collapses each mapping subscript to a single call. + { + let fn_ty = i256_ty.fn_type(&[i256_ty.into(), i256_ty.into(), i32_ty.into()], false); + let func = bin + .module + .add_function("__map_slot", fn_ty, Some(Linkage::Internal)); + func.add_attribute(inkwell::attributes::AttributeLoc::Function, noinline); + func.add_attribute(inkwell::attributes::AttributeLoc::Function, readnone); + let entry = context.append_basic_block(func, "entry"); + bin.builder.position_at_end(entry); + + let prev = func.get_nth_param(0).unwrap().into_int_value(); + let key = func.get_nth_param(1).unwrap().into_int_value(); + let key_len = func.get_nth_param(2).unwrap().into_int_value(); + + // preimage buffer: prev(32) ‖ key(32); only 32 + key_len bytes are hashed. + let buf = bin + .builder + .build_array_alloca(i8_ty, i32_ty.const_int(64, false), "preimage") + .unwrap(); + bin.builder.build_store(buf, prev).unwrap(); + let key_ptr = unsafe { + bin.builder + .build_gep(i8_ty, buf, &[i32_ty.const_int(32, false)], "key_ptr") + .unwrap() + }; + bin.builder.build_store(key_ptr, key).unwrap(); + let total = bin + .builder + .build_int_add(i32_ty.const_int(32, false), key_len, "preimage_len") + .unwrap(); + + let dst = bin.builder.build_alloca(i256_ty, "slot_hash").unwrap(); + let sha3 = bin.module.get_function("sha3").unwrap(); + bin.builder + .build_call( + sha3, + &[ + buf.into(), + total.into(), + dst.into(), + i32_ty.const_int(32, false).into(), // hash_len = 32 + i32_ty.const_int(1, false).into(), // keccak256 mode + ], + "", + ) + .unwrap(); + let hash = bin.builder.build_load(i256_ty, dst, "hash").unwrap(); + bin.builder.build_return(Some(&hash)).unwrap(); + } + } + + fn emit_functions<'a>(contract: &'a ast::Contract, bin: &mut Binary<'a>) { + let mut defines = Vec::new(); + + for (cfg_no, cfg) in contract.cfg.iter().enumerate() { + let ftype = bin.function_type( + &cfg.params.iter().map(|p| p.ty.clone()).collect::>(), + &cfg.returns.iter().map(|p| p.ty.clone()).collect::>(), + ); + + // All user functions are internal; apply() is the only export. + let func_decl = if let Some(func) = bin.module.get_function(&cfg.name) { + assert_eq!(func.get_first_basic_block(), None); + func + } else { + bin.module + .add_function(&cfg.name, ftype, Some(Linkage::Internal)) + }; + + bin.functions.insert(cfg_no, func_decl); + defines.push((func_decl, cfg)); + } + + for (func_decl, cfg) in defines { + emit_cfg(&mut AntelopeTarget, bin, contract, cfg, func_decl); + } + } + + /// Compute the byte size of a fixed-size type in Antelope DataStream serialization. + /// Returns None for variable-length types (string, bytes). + /// Shared authority for fixed-size widths — used by action-data deserialization, + /// return-value serialization, and `antelope.pack` so they cannot drift. + pub(crate) fn datastream_fixed_size(ty: &Type, bin: &Binary) -> Option { + match ty { + Type::Bool => Some(1), + Type::Uint(n) | Type::Int(n) => Some(((*n as u32) + 7) / 8), + Type::Bytes(n) => Some(*n as u32), + Type::Enum(n) => { + let bits = bin.ns.enums[*n].ty.bits(bin.ns) as u32; + Some((bits + 7) / 8) + } + Type::Value => Some(bin.ns.value_length as u32), + Type::Contract(_) | Type::Address(_) => Some(bin.ns.address_length as u32), + Type::String | Type::DynamicBytes => None, + _ => panic!( + "Antelope: unsupported parameter type for action data deserialization: {ty:?}" + ), + } + } + + /// Check if any parameter in the list requires variable-length deserialization. + fn has_variable_length_params(params: &[ast::Parameter], bin: &Binary) -> bool { + params + .iter() + .any(|p| Self::datastream_fixed_size(&p.ty, bin).is_none()) + } + + /// Emit the `apply(receiver, code, action)` entry point. + /// Dispatches to the correct function based on the `action` parameter. + fn emit_apply<'a>( + context: &'a Context, + bin: &mut Binary<'a>, + contract: &'a ast::Contract, + export_list: &mut Vec<&'a str>, + ) { + let i64_ty = context.i64_type(); + let void_ty = context.void_type(); + + let apply_ty = + void_ty.fn_type(&[i64_ty.into(), i64_ty.into(), i64_ty.into()], false); + let apply_func = bin + .module + .add_function("apply", apply_ty, Some(Linkage::External)); + export_list.push("apply"); + + let entry = context.append_basic_block(apply_func, "entry"); + bin.builder.position_at_end(entry); + + let receiver = apply_func.get_nth_param(0).unwrap().into_int_value(); + let code = apply_func.get_nth_param(1).unwrap().into_int_value(); + let action = apply_func.get_nth_param(2).unwrap().into_int_value(); + + // Initialize the heap allocator (linked-list at HEAP_START=0x10000). + // Must be called before any code that allocates (strings, dynamic arrays, etc.). + if let Some(init_heap) = bin.module.get_function("__init_heap") { + bin.builder.build_call(init_heap, &[], "").unwrap(); + } + + // Store receiver and code in globals for use by builtins. + let receiver_global = Self::get_receiver_global(bin); + bin.builder + .build_store(receiver_global.as_pointer_value(), receiver) + .unwrap(); + let code_global = bin.module.get_global("__code").unwrap(); + bin.builder + .build_store(code_global.as_pointer_value(), code) + .unwrap(); + + // Dispatch actions regardless of code == receiver. + // When code == receiver: normal action call. + // When code != receiver: notification (from require_recipient). + // Both paths dispatch to the same action handlers. + let dispatch_bb = context.append_basic_block(apply_func, "dispatch"); + let return_bb = context.append_basic_block(apply_func, "return"); + + bin.builder + .build_unconditional_branch(dispatch_bb) + .unwrap(); + + bin.builder.position_at_end(dispatch_bb); + + // For each public function, compare action name and call if matched. + for cfg in &contract.cfg { + if !cfg.public || cfg.is_placeholder() { + continue; + } + + // Extract the short function name (after last "::" if mangled). + let func_name = if cfg.name.contains("::") { + cfg.name.split("::").last().unwrap_or(&cfg.name) + } else { + &cfg.name + }; + + // Skip constructor-like functions (they contain hex selectors). + if func_name.starts_with("constructor") { + continue; + } + + // Strip mangled parameter suffix: "record__uint64_uint64" → "record" + let action_name = func_name.split("__").next().unwrap_or(func_name); + // Normalize to eosio::name charset (lowercase + 1-5 + dot, max 12 chars) + // to match the ABI action name generation. + let action_name_normalized = + crate::abi::antelope::normalize_action_name(action_name); + let action_encoded = string_to_name(&action_name_normalized); + + let action_const = i64_ty.const_int(action_encoded, false); + let matches = bin + .builder + .build_int_compare(IntPredicate::EQ, action, action_const, "action_match") + .unwrap(); + + let call_bb = + context.append_basic_block(apply_func, &format!("call_{action_name}")); + let next_bb = context.append_basic_block(apply_func, "next"); + + bin.builder + .build_conditional_branch(matches, call_bb, next_bb) + .unwrap(); + + bin.builder.position_at_end(call_bb); + + if let Some(func) = bin.module.get_function(&cfg.name) { + let mut args: Vec = Vec::new(); + + if !cfg.params.is_empty() { + // Read raw action data into a stack buffer. + let i32_ty = context.i32_type(); + let data_size_fn = + bin.module.get_function("action_data_size").unwrap(); + let data_size = bin + .builder + .build_call(data_size_fn, &[], "data_size") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let data_buf = bin + .builder + .build_array_alloca( + context.i8_type(), + data_size, + "action_data", + ) + .unwrap(); + + let read_fn = + bin.module.get_function("read_action_data").unwrap(); + bin.builder + .build_call( + read_fn, + &[data_buf.into(), data_size.into()], + "", + ) + .unwrap(); + + // Use dynamic offset for deserialization (needed for variable-length types). + let offset_alloca = bin + .builder + .build_alloca(i32_ty, "ds_offset") + .unwrap(); + bin.builder + .build_store(offset_alloca, i32_ty.const_zero()) + .unwrap(); + + for param in cfg.params.iter() { + let cur_offset = bin + .builder + .build_load(i32_ty, offset_alloca, "cur_off") + .unwrap() + .into_int_value(); + + let param_ptr = unsafe { + bin.builder + .build_gep( + context.i8_type(), + data_buf, + &[cur_offset], + "param_ptr", + ) + .unwrap() + }; + + if let Some(byte_size) = Self::datastream_fixed_size(¶m.ty, bin) { + // Fixed-size type: load directly from buffer. + let llvm_ty = bin.llvm_var_ty(¶m.ty); + let param_val = bin + .builder + .build_load(llvm_ty, param_ptr, "param") + .unwrap(); + args.push(param_val.into()); + + let new_offset = bin + .builder + .build_int_add( + cur_offset, + i32_ty.const_int(byte_size as u64, false), + "new_off", + ) + .unwrap(); + bin.builder + .build_store(offset_alloca, new_offset) + .unwrap(); + } else { + // Variable-length type (string/bytes): read varuint32 length, then data. + // Antelope DataStream encodes strings as: varuint32 length + raw bytes. + // Use __decode_varuint32 to handle lengths >= 128 correctly. + // Returns u64 packed as: low32 = value, high32 = bytes_read. + let decode_fn = bin.module.get_function("__decode_varuint32").unwrap(); + let packed = bin + .builder + .build_call(decode_fn, &[param_ptr.into()], "vdec") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // str_len = low 32 bits + let str_len_i64 = bin.builder + .build_and(packed, i64_ty.const_int(0xFFFF_FFFF, false), "sl64") + .unwrap(); + let str_len_i32 = bin.builder + .build_int_truncate(str_len_i64, i32_ty, "str_len") + .unwrap(); + + // bytes_read = high 32 bits + let bytes_read_i64 = bin.builder + .build_right_shift(packed, i64_ty.const_int(32, false), false, "br64") + .unwrap(); + let bytes_read = bin.builder + .build_int_truncate(bytes_read_i64, i32_ty, "bytes_read") + .unwrap(); + + // Advance past the varuint32 header. + let after_len = bin + .builder + .build_int_add(cur_offset, bytes_read, "after_len") + .unwrap(); + + let str_data_ptr = unsafe { + bin.builder + .build_gep( + context.i8_type(), + data_buf, + &[after_len], + "str_data", + ) + .unwrap() + }; + + // Allocate a vector on the heap: vector_new(len, 1, data_ptr) + let vector_new_fn = + bin.module.get_function("vector_new").unwrap(); + let vec_ptr = bin + .builder + .build_call( + vector_new_fn, + &[ + str_len_i32.into(), + i32_ty.const_int(1, false).into(), + str_data_ptr.into(), + ], + "str_vec", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap(); + + args.push(vec_ptr.into()); + + // Advance offset past varuint32 header + string data. + let new_offset = bin + .builder + .build_int_add(after_len, str_len_i32, "new_off") + .unwrap(); + bin.builder + .build_store(offset_alloca, new_offset) + .unwrap(); + } + } + } + + // Add output pointers for return values (passed by pointer). + let mut ret_allocas = Vec::new(); + for ret in cfg.returns.iter() { + let ret_alloca = bin + .builder + .build_alloca(bin.llvm_var_ty(&ret.ty), "ret") + .unwrap(); + ret_allocas.push((ret_alloca, ret.ty.clone())); + args.push(ret_alloca.into()); + } + + bin.builder.build_call(func, &args, "").unwrap(); + + // Serialize return values via set_action_return_value (Leap 3.x+). + if !ret_allocas.is_empty() { + let i32_ty = context.i32_type(); + // Compute total byte size of all fixed-size returns. + let mut total_size: u32 = 0; + let mut all_fixed = true; + for (_, ty) in &ret_allocas { + if let Some(sz) = Self::datastream_fixed_size(ty, bin) { + total_size += sz; + } else { + all_fixed = false; + break; + } + } + + if all_fixed && total_size > 0 { + let set_return_fn = bin + .module + .get_function("set_action_return_value") + .unwrap(); + + if ret_allocas.len() == 1 { + // Single return: pass alloca pointer directly. + let (alloca, _) = &ret_allocas[0]; + bin.builder + .build_call( + set_return_fn, + &[ + (*alloca).into(), + i32_ty + .const_int(total_size as u64, false) + .into(), + ], + "", + ) + .unwrap(); + } else { + // Multiple returns: pack into a contiguous buffer. + let ret_buf = bin + .builder + .build_array_alloca( + context.i8_type(), + i32_ty.const_int(total_size as u64, false), + "ret_buf", + ) + .unwrap(); + let mut offset: u32 = 0; + for (alloca, ty) in &ret_allocas { + let sz = + Self::datastream_fixed_size(ty, bin).unwrap(); + let dest = unsafe { + bin.builder + .build_gep( + context.i8_type(), + ret_buf, + &[i32_ty + .const_int(offset as u64, false)], + "ret_dest", + ) + .unwrap() + }; + bin.builder + .build_call( + // The runtime memcpy is declared as `__memcpy` + // (see declare_externals); a bare `memcpy` was + // never declared and panicked the multi-return + // path (e.g. a public struct-returning getter). + bin.module + .get_function("__memcpy") + .unwrap(), + &[ + dest.into(), + (*alloca).into(), + i32_ty + .const_int(sz as u64, false) + .into(), + ], + "", + ) + .unwrap(); + offset += sz; + } + bin.builder + .build_call( + set_return_fn, + &[ + ret_buf.into(), + i32_ty + .const_int(total_size as u64, false) + .into(), + ], + "", + ) + .unwrap(); + } + } + } + } + bin.builder.build_unconditional_branch(return_bb).unwrap(); + + bin.builder.position_at_end(next_bb); + } + + // Fall through (no action matched) — just return. + bin.builder.build_unconditional_branch(return_bb).unwrap(); + + bin.builder.position_at_end(return_bb); + bin.builder.build_return(None).unwrap(); + } +} diff --git a/src/emit/antelope/target.rs b/src/emit/antelope/target.rs new file mode 100644 index 000000000..c26fe84c5 --- /dev/null +++ b/src/emit/antelope/target.rs @@ -0,0 +1,2141 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::codegen::cfg::HashTy; +use crate::codegen::{Builtin, Expression}; +use crate::emit::antelope::{AntelopeTarget, STATE_TABLE_NAME}; +use crate::emit::binary::Binary; +use crate::emit::ContractArgs; +use crate::emit::{TargetRuntime, Variable}; +use crate::sema::ast; +use crate::sema::ast::CallTy; +use crate::sema::ast::{Function, RetrieveType, Type}; + +use inkwell::types::{BasicTypeEnum, IntType}; +use inkwell::values::{ + ArrayValue, BasicMetadataValueEnum, BasicValueEnum, FunctionValue, IntValue, PointerValue, +}; +use inkwell::IntPredicate; + +use solang_parser::pt::{Loc, StorageType}; + +use num_traits::{ToPrimitive, Zero}; +use std::collections::HashMap; + +// Antelope TargetRuntime implementation. +// Storage model: one "state" table per contract. +// Lookup via idx256 secondary index (full 256-bit slot hash). +// Auto-increment primary key (like CDT available_primary_key). +// Row data = raw value bytes only. +#[allow(unused_variables)] +impl<'a> TargetRuntime<'a> for AntelopeTarget { + fn get_storage_int( + &self, + bin: &Binary<'a>, + function: FunctionValue, + slot: PointerValue<'a>, + ty: IntType<'a>, + ) -> IntValue<'a> { + todo!("antelope: get_storage_int") + } + + /// Load a value from Antelope table storage via idx256 secondary index. + /// + /// 1. receiver = load __receiver global + /// 2. Convert slot to 256-bit hash, store to stack buffer + /// 3. sec_iter = db_idx256_find_secondary(receiver, receiver, table, slot_buf, 2, &pk) + /// 4. if sec_iter >= 0: db_find_i64(pk) → db_get_i64 → return value + /// 5. else: return zero + fn storage_load( + &self, + bin: &Binary<'a>, + ty: &ast::Type, + slot: &mut IntValue<'a>, + function: FunctionValue<'a>, + storage_type: &Option, + ) -> BasicValueEnum<'a> { + // Struct: load each field recursively at consecutive slots. + if let Type::Struct(struct_type) = ty { + let struct_def = struct_type.definition(bin.ns); + let llvm_ty = bin.llvm_type(ty); + let struct_ptr = bin.build_alloca(function, llvm_ty, "struct_alloc"); + + let mut current_slot = *slot; + for (i, field) in struct_def.fields.iter().enumerate() { + if field.infinite_size { + continue; + } + let field_val = + self.storage_load(bin, &field.ty, &mut current_slot, function, storage_type); + let field_ptr = bin + .builder + .build_struct_gep(llvm_ty.into_struct_type(), struct_ptr, i as u32, "field_ptr") + .unwrap(); + bin.builder.build_store(field_ptr, field_val).unwrap(); + // Advance slot by number of storage slots this field occupies. + let slots = field.ty.storage_slots(bin.ns); + if !slots.is_zero() { + let slot_inc = bin + .context + .custom_width_int_type(256) + .const_int(slots.to_u64().unwrap_or(1), false); + current_slot = bin + .builder + .build_int_add(current_slot, slot_inc, "next_slot") + .unwrap(); + } + } + return bin + .builder + .build_load(llvm_ty, struct_ptr, "struct_loaded") + .unwrap(); + } + + // String/DynamicBytes: variable-length row. + if matches!(ty, Type::String | Type::DynamicBytes) { + return self + .storage_load_string(bin, slot, function) + .into(); + } + + let i32_ty = bin.context.i32_type(); + let i256_ty = bin.context.custom_width_int_type(256); + + let bits = ty.bits(bin.ns) as u32; + let byte_size = (bits + 7) / 8; + let val_ty = bin.context.custom_width_int_type(bits); + + // Convert slot to a 256-bit value for the shared helper. + let slot_i256 = if slot.get_type().get_bit_width() == 256 { + *slot + } else if slot.get_type().get_bit_width() > 256 { + bin.builder.build_int_truncate(*slot, i256_ty, "slot256").unwrap() + } else { + bin.builder.build_int_z_extend(*slot, i256_ty, "slot256").unwrap() + }; + + // Pre-zero the value buffer so an unset slot reads as 0, then let the shared + // helper overwrite it on a hit. Collapses the inline find/get sequence to a call. + let buf = bin + .builder + .build_array_alloca( + bin.context.i8_type(), + i32_ty.const_int(byte_size as u64, false), + "load_buf", + ) + .unwrap(); + bin.builder.build_store(buf, val_ty.const_zero()).unwrap(); + + let helper = bin.module.get_function("__antelope_load_slot").unwrap(); + bin.builder + .build_call( + helper, + &[ + slot_i256.into(), + buf.into(), + i32_ty.const_int(byte_size as u64, false).into(), + ], + "", + ) + .unwrap(); + + bin.builder.build_load(val_ty, buf, "storage_val").unwrap() + } + + /// Store a value to Antelope table storage via idx256 secondary index. + /// + /// 1. receiver = load __receiver global + /// 2. Convert slot to 256-bit hash buffer + /// 3. sec_iter = db_idx256_find_secondary(receiver, receiver, table, slot_buf, 2, &pk) + /// 4. if found: db_find_i64(pk) → db_update_i64(pri_iter, receiver, &val, size) + /// 5. else: new_pk = available_primary_key() via db_end_i64/db_previous_i64 + /// db_store_i64(new_pk, &val) + db_idx256_store(new_pk, slot_buf) + fn storage_store( + &self, + bin: &Binary<'a>, + ty: &ast::Type, + existing: bool, + slot: &mut IntValue<'a>, + dest: BasicValueEnum<'a>, + function: FunctionValue<'a>, + storage_type: &Option, + ) { + // Struct: store each field recursively at consecutive slots. + if let Type::Struct(struct_type) = ty { + let struct_def = struct_type.definition(bin.ns); + let llvm_ty = bin.llvm_type(ty); + + // dest may be a pointer to the struct (heap-allocated) or a struct value. + let struct_ptr = if dest.is_pointer_value() { + // Already a pointer — use it directly. + dest.into_pointer_value() + } else { + // Struct value — store to temp alloca so we can GEP fields. + let tmp = bin.build_alloca(function, llvm_ty, "struct_tmp"); + bin.builder.build_store(tmp, dest).unwrap(); + tmp + }; + + let mut current_slot = *slot; + for (i, field) in struct_def.fields.iter().enumerate() { + if field.infinite_size { + continue; + } + let field_ptr = bin + .builder + .build_struct_gep(llvm_ty.into_struct_type(), struct_ptr, i as u32, "field_ptr") + .unwrap(); + let field_val = bin + .builder + .build_load(bin.llvm_type(&field.ty), field_ptr, "field_val") + .unwrap(); + self.storage_store( + bin, + &field.ty, + existing, + &mut current_slot, + field_val, + function, + storage_type, + ); + let slots = field.ty.storage_slots(bin.ns); + if !slots.is_zero() { + let slot_inc = bin + .context + .custom_width_int_type(256) + .const_int(slots.to_u64().unwrap_or(1), false); + current_slot = bin + .builder + .build_int_add(current_slot, slot_inc, "next_slot") + .unwrap(); + } + } + return; + } + + // String/DynamicBytes: variable-length row. + if matches!(ty, Type::String | Type::DynamicBytes) { + self.storage_store_string(bin, slot, dest, function); + return; + } + + let i32_ty = bin.context.i32_type(); + let i256_ty = bin.context.custom_width_int_type(256); + + let bits = ty.bits(bin.ns) as u32; + let byte_size = (bits + 7) / 8; + let val_ty = bin.context.custom_width_int_type(bits); + assert!(byte_size <= 127, "fixed-size value too large for 1-byte varuint32"); + + // Convert slot to a 256-bit value for the shared helper. + let slot_i256 = if slot.get_type().get_bit_width() == 256 { + *slot + } else if slot.get_type().get_bit_width() > 256 { + bin.builder.build_int_truncate(*slot, i256_ty, "slot256").unwrap() + } else { + bin.builder.build_int_z_extend(*slot, i256_ty, "slot256").unwrap() + }; + + // Materialize the value into a little-endian byte buffer, then hand off to the + // shared helper, which does the find + update-or-insert row write. + let dest_int = dest.into_int_value(); + let store_val = if dest_int.get_type().get_bit_width() == bits { + dest_int + } else if dest_int.get_type().get_bit_width() > bits { + bin.builder.build_int_truncate(dest_int, val_ty, "trunc").unwrap() + } else { + bin.builder.build_int_z_extend(dest_int, val_ty, "extend").unwrap() + }; + let buf = bin + .builder + .build_array_alloca( + bin.context.i8_type(), + i32_ty.const_int(byte_size as u64, false), + "store_buf", + ) + .unwrap(); + bin.builder.build_store(buf, store_val).unwrap(); + + let helper = bin.module.get_function("__antelope_store_slot").unwrap(); + bin.builder + .build_call( + helper, + &[ + slot_i256.into(), + buf.into(), + i32_ty.const_int(byte_size as u64, false).into(), + ], + "", + ) + .unwrap(); + } + + /// Delete a value from Antelope table storage via idx256 secondary index. + /// + /// 1. Find via idx256: db_idx256_find_secondary(slot_hash) → sec_iter, pk + /// 2. If found: db_find_i64(pk) → pri_iter, then db_remove_i64(pri_iter) + db_idx256_remove(sec_iter) + /// 3. If not found: no-op (deleting non-existent slot is fine) + fn storage_delete( + &self, + bin: &Binary<'a>, + ty: &Type, + slot: &mut IntValue<'a>, + function: FunctionValue<'a>, + ) { + // Struct: delete each field recursively at consecutive slots. + if let Type::Struct(struct_type) = ty { + let struct_def = struct_type.definition(bin.ns); + let mut current_slot = *slot; + for field in &struct_def.fields { + if field.infinite_size { + continue; + } + self.storage_delete(bin, &field.ty, &mut current_slot, function); + let slots = field.ty.storage_slots(bin.ns); + if !slots.is_zero() { + let slot_inc = bin + .context + .custom_width_int_type(256) + .const_int(slots.to_u64().unwrap_or(1), false); + current_slot = bin + .builder + .build_int_add(current_slot, slot_inc, "next_slot") + .unwrap(); + } + } + return; + } + + let i32_ty = bin.context.i32_type(); + let i64_ty = bin.context.i64_type(); + let i256_ty = bin.context.custom_width_int_type(256); + + // Load receiver. + let receiver_global = AntelopeTarget::get_receiver_global(bin); + let receiver = bin + .builder + .build_load(i64_ty, receiver_global.as_pointer_value(), "receiver") + .unwrap() + .into_int_value(); + + let table_name = i64_ty.const_int(STATE_TABLE_NAME, false); + + // Convert slot to 256-bit value and store to stack buffer. + let slot_i256 = if slot.get_type().get_bit_width() == 256 { + *slot + } else if slot.get_type().get_bit_width() > 256 { + bin.builder + .build_int_truncate(*slot, i256_ty, "slot256") + .unwrap() + } else { + bin.builder + .build_int_z_extend(*slot, i256_ty, "slot256") + .unwrap() + }; + + let slot_buf = bin + .builder + .build_array_alloca( + bin.context.i8_type(), + i32_ty.const_int(32, false), + "slot_buf", + ) + .unwrap(); + bin.builder.build_store(slot_buf, slot_i256).unwrap(); + + // Look up via idx256. + let pk_out = bin.builder.build_alloca(i64_ty, "pk_out").unwrap(); + let db_idx256_find = bin + .module + .get_function("db_idx256_find_secondary") + .unwrap(); + let data_len = i32_ty.const_int(2, false); + let sec_iter = bin + .builder + .build_call( + db_idx256_find, + &[ + receiver.into(), + receiver.into(), + table_name.into(), + slot_buf.into(), + data_len.into(), + pk_out.into(), + ], + "sec_iter", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let found = bin + .builder + .build_int_compare( + IntPredicate::SGE, + sec_iter, + i32_ty.const_zero(), + "found", + ) + .unwrap(); + + let delete_bb = bin.context.append_basic_block(function, "del_found"); + let done_bb = bin.context.append_basic_block(function, "del_done"); + + bin.builder + .build_conditional_branch(found, delete_bb, done_bb) + .unwrap(); + + // FOUND: remove both primary row and secondary index entry. + bin.builder.position_at_end(delete_bb); + + let pk = bin + .builder + .build_load(i64_ty, pk_out, "pk") + .unwrap() + .into_int_value(); + + // Find primary row iterator. + let db_find = bin.module.get_function("db_find_i64").unwrap(); + let pri_iter = bin + .builder + .build_call( + db_find, + &[ + receiver.into(), + receiver.into(), + table_name.into(), + pk.into(), + ], + "pri_iter", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // db_remove_i64(pri_iter) + let db_remove = bin.module.get_function("db_remove_i64").unwrap(); + bin.builder + .build_call(db_remove, &[pri_iter.into()], "") + .unwrap(); + + // db_idx256_remove(sec_iter) + let db_idx256_remove = bin.module.get_function("db_idx256_remove").unwrap(); + bin.builder + .build_call(db_idx256_remove, &[sec_iter.into()], "") + .unwrap(); + + bin.builder.build_unconditional_branch(done_bb).unwrap(); + + bin.builder.position_at_end(done_bb); + } + + fn set_storage_string( + &self, + bin: &Binary<'a>, + function: FunctionValue<'a>, + slot: PointerValue<'a>, + dest: BasicValueEnum<'a>, + ) { + // Load the slot as i256 from the pointer. + let i256_ty = bin.context.custom_width_int_type(256); + let slot_val = bin + .builder + .build_load(i256_ty, slot, "slot_val") + .unwrap() + .into_int_value(); + let mut slot_mut = slot_val; + self.storage_store_string(bin, &mut slot_mut, dest.into(), function); + } + + fn get_storage_string( + &self, + bin: &Binary<'a>, + function: FunctionValue, + slot: PointerValue<'a>, + ) -> PointerValue<'a> { + let i256_ty = bin.context.custom_width_int_type(256); + let slot_val = bin + .builder + .build_load(i256_ty, slot, "slot_val") + .unwrap() + .into_int_value(); + let mut slot_mut = slot_val; + self.storage_load_string(bin, &mut slot_mut, function) + } + + fn set_storage_extfunc( + &self, + bin: &Binary<'a>, + function: FunctionValue, + slot: PointerValue, + dest: PointerValue, + dest_ty: BasicTypeEnum, + ) { + todo!("antelope: set_storage_extfunc") + } + + fn get_storage_extfunc( + &self, + bin: &Binary<'a>, + function: FunctionValue, + slot: PointerValue<'a>, + ) -> PointerValue<'a> { + todo!("antelope: get_storage_extfunc") + } + + fn get_storage_bytes_subscript( + &self, + bin: &Binary<'a>, + function: FunctionValue, + slot: IntValue<'a>, + index: IntValue<'a>, + loc: Loc, + ) -> IntValue<'a> { + todo!("antelope: get_storage_bytes_subscript") + } + + fn set_storage_bytes_subscript( + &self, + bin: &Binary<'a>, + function: FunctionValue, + slot: IntValue<'a>, + index: IntValue<'a>, + value: IntValue<'a>, + loc: Loc, + ) { + todo!("antelope: set_storage_bytes_subscript") + } + + fn storage_subscript( + &self, + bin: &Binary<'a>, + function: FunctionValue<'a>, + ty: &Type, + slot: IntValue<'a>, + index: BasicValueEnum<'a>, + ) -> IntValue<'a> { + todo!("antelope: storage_subscript") + } + + fn storage_push( + &self, + bin: &Binary<'a>, + function: FunctionValue<'a>, + ty: &Type, + slot: IntValue<'a>, + val: Option>, + ) -> BasicValueEnum<'a> { + todo!("antelope: storage_push") + } + + fn storage_pop( + &self, + bin: &Binary<'a>, + function: FunctionValue<'a>, + ty: &Type, + slot: IntValue<'a>, + load: bool, + loc: Loc, + ) -> Option> { + todo!("antelope: storage_pop") + } + + fn storage_array_length( + &self, + _bin: &Binary<'a>, + _function: FunctionValue, + _slot: IntValue<'a>, + _elem_ty: &Type, + ) -> IntValue<'a> { + todo!("antelope: storage_array_length") + } + + /// Hash using Antelope's sha3 host function in keccak256 mode. + /// Matches Ethereum's keccak256 for storage slot derivation. + /// Requires CRYPTO_PRIMITIVES protocol feature (active on EOS mainnet). + fn keccak256_hash( + &self, + bin: &Binary<'a>, + src: PointerValue, + length: IntValue, + dest: PointerValue, + ) { + let sha3_fn = bin.module.get_function("sha3").unwrap(); + let i32_ty = bin.context.i32_type(); + let len_i32 = if length.get_type().get_bit_width() == 32 { + length + } else { + bin.builder + .build_int_truncate(length, i32_ty, "len32") + .unwrap() + }; + let hash_len = i32_ty.const_int(32, false); // checksum256 = 32 bytes + let keccak_flag = i32_ty.const_int(1, false); // 1 = keccak256 mode + bin.builder + .build_call( + sha3_fn, + &[src.into(), len_i32.into(), dest.into(), hash_len.into(), keccak_flag.into()], + "", + ) + .unwrap(); + } + + /// Print a string by calling the Antelope `prints_l(msg, len)` host function. + fn print<'b>(&self, bin: &Binary<'b>, string: PointerValue<'b>, length: IntValue<'b>) { + let prints_l = bin.module.get_function("prints_l").unwrap(); + + let len_i32 = if length.get_type().get_bit_width() == 32 { + length + } else { + bin.builder + .build_int_truncate(length, bin.context.i32_type(), "len32") + .unwrap() + }; + + bin.builder + .build_call(prints_l, &[string.into(), len_i32.into()], "") + .unwrap(); + } + + fn return_empty_abi(&self, bin: &Binary) {} + + fn return_code<'b>(&self, bin: &'b Binary, _ret: IntValue<'b>) { + // Antelope has no selector return-code path — the real entrypoint is the + // hand-written `apply`, so `Instr::ReturnCode` only appears in the inherited + // (dead) dispatch CFGs. Still, this must emit a terminator: an empty body left + // the `fb_or_recv` block unterminated, producing invalid IR that crashed the + // backend at -O none/less (it was only masked at -O default by global_dce). + // Mirror the Polkadot target: abort via assert_failure, which ends in unreachable. + let ptr_null = bin + .context + .ptr_type(inkwell::AddressSpace::default()) + .const_null(); + let zero = bin.context.i32_type().const_zero(); + self.assert_failure(bin, ptr_null, zero); + } + + fn assert_failure(&self, bin: &Binary, data: PointerValue, length: IntValue) { + let eosio_assert = bin.module.get_function("eosio_assert").unwrap(); + let zero = bin.context.i32_type().const_zero(); + let msg = bin.emit_global_string("assert_failure", b"assertion failed\0", true); + bin.builder + .build_call(eosio_assert, &[zero.into(), msg.into()], "") + .unwrap(); + bin.builder.build_unreachable().unwrap(); + } + + fn builtin_function( + &self, + bin: &Binary<'a>, + function: FunctionValue<'a>, + builtin_func: &Function, + args: &[BasicMetadataValueEnum<'a>], + first_arg_type: Option, + ) -> Option> { + todo!("antelope: builtin_function") + } + + fn create_contract<'b>( + &mut self, + bin: &Binary<'b>, + function: FunctionValue<'b>, + success: Option<&mut BasicValueEnum<'b>>, + contract_no: usize, + address: PointerValue<'b>, + encoded_args: BasicValueEnum<'b>, + encoded_args_len: BasicValueEnum<'b>, + contract_args: ContractArgs<'b>, + loc: Loc, + ) { + todo!("antelope: create_contract") + } + + fn external_call<'b>( + &self, + bin: &Binary<'b>, + function: FunctionValue<'b>, + success: Option<&mut BasicValueEnum<'b>>, + payload: PointerValue<'b>, + payload_len: IntValue<'b>, + address: Option>, + contract_args: ContractArgs<'b>, + ty: CallTy, + loc: Loc, + ) { + todo!("antelope: external_call") + } + + fn value_transfer<'b>( + &self, + _bin: &Binary<'b>, + _function: FunctionValue, + _success: Option<&mut BasicValueEnum<'b>>, + _address: PointerValue<'b>, + _value: IntValue<'b>, + loc: Loc, + ) { + unimplemented!("antelope: value_transfer not supported") + } + + fn builtin<'b>( + &self, + bin: &Binary<'b>, + expr: &Expression, + vartab: &HashMap>, + function: FunctionValue<'b>, + ) -> BasicValueEnum<'b> { + match expr { + Expression::Builtin { + kind: Builtin::AntelopeRequireAuth, + args, + .. + } => { + let account = crate::emit::expression::expression( + &AntelopeTarget, + bin, + &args[0], + vartab, + function, + ) + .into_int_value(); + + let require_auth_fn = bin.module.get_function("require_auth").unwrap(); + bin.builder + .build_call(require_auth_fn, &[account.into()], "") + .unwrap(); + + // requireAuth returns void; return a dummy value. + bin.context + .i64_type() + .const_zero() + .into() + } + Expression::Builtin { + kind: Builtin::AntelopeSelf, + .. + } => { + let i64_ty = bin.context.i64_type(); + let receiver_global = AntelopeTarget::get_receiver_global(bin); + bin.builder + .build_load(i64_ty, receiver_global.as_pointer_value(), "self_recv") + .unwrap() + } + Expression::Builtin { + kind: Builtin::AntelopeCode, + .. + } => { + let i64_ty = bin.context.i64_type(); + let code_global = bin.module.get_global("__code").unwrap(); + bin.builder + .build_load(i64_ty, code_global.as_pointer_value(), "code_acct") + .unwrap() + } + Expression::Builtin { + kind: Builtin::AntelopeRequireRecipient, + args, + .. + } => { + let account = crate::emit::expression::expression( + &AntelopeTarget, + bin, + &args[0], + vartab, + function, + ) + .into_int_value(); + + let require_recipient_fn = + bin.module.get_function("require_recipient").unwrap(); + bin.builder + .build_call(require_recipient_fn, &[account.into()], "") + .unwrap(); + + bin.context.i64_type().const_zero().into() + } + Expression::Builtin { + kind: Builtin::AntelopeCall, + args, + .. + } => { + // antelope.call(contract, action_name, packed_data) + // Serializes an Antelope action struct and calls send_inline. + // + // Antelope serialized action format: + // account: uint64 (8 bytes) — target contract + // action_name: uint64 (8 bytes) — action name + // auth_count: varuint32(1 byte) — number of permission entries (we use 1) + // auth[0].actor: uint64 (8 bytes) — self (current contract) + // auth[0].permission: uint64 (8 bytes) — eosio::name("active") = 0x3232EDA800000000 + // data_len: varuint32(1 byte) — length of packed action data + // data: bytes — raw packed action data + // + // Total header = 8 + 8 + 1 + 8 + 8 + 1 = 34 bytes, then data. + + let i32_ty = bin.context.i32_type(); + let i64_ty = bin.context.i64_type(); + let i8_ty = bin.context.i8_type(); + + let contract_account = crate::emit::expression::expression( + &AntelopeTarget, + bin, + &args[0], + vartab, + function, + ) + .into_int_value(); + + let action_name = crate::emit::expression::expression( + &AntelopeTarget, + bin, + &args[1], + vartab, + function, + ) + .into_int_value(); + + // The third argument is a bytes vector (pointer to vector struct). + let data_vec = crate::emit::expression::expression( + &AntelopeTarget, + bin, + &args[2], + vartab, + function, + ); + + // Get data length and data pointer from the vector. + // Vector layout: [length: u32, ...data] + let data_len = bin.vector_len(data_vec); + + let data_ptr = bin.vector_bytes(data_vec); + + // Load receiver (self) for the authorization. + let receiver_global = AntelopeTarget::get_receiver_global(bin); + let self_account = bin + .builder + .build_load(i64_ty, receiver_global.as_pointer_value(), "self_acct") + .unwrap() + .into_int_value(); + + let active_perm = i64_ty.const_int(crate::emit::antelope::string_to_name("active"), false); + + // Encode data_len as varuint32 to know its byte size. + let encode_fn = bin.module.get_function("__encode_varuint32").unwrap(); + let vi_tmp = bin + .builder + .build_array_alloca(i8_ty, i32_ty.const_int(5, false), "vi_tmp") + .unwrap(); + let data_vi_size = bin + .builder + .build_call(encode_fn, &[vi_tmp.into(), data_len.into()], "dvi_sz") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // Serialized action layout: + // account(8) + action_name(8) + auth_count(1=varuint for 1) + // + actor(8) + permission(8) + data_len(varuint) + data(N) + // Fixed part = 8+8+1+8+8 = 33, then data_vi_size + data_len + let fixed_part = i32_ty.const_int(33, false); + let total_size = bin.builder.build_int_add(fixed_part, data_vi_size, "ts1").unwrap(); + let total_size = bin.builder.build_int_add(total_size, data_len, "total_size").unwrap(); + + let malloc_fn = bin.module.get_function("__malloc").unwrap(); + let buf = bin + .builder + .build_call(malloc_fn, &[total_size.into()], "action_buf") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_pointer_value(); + + // Write account (offset 0) + bin.builder.build_store(buf, contract_account).unwrap(); + + // Write action_name (offset 8) + let off8 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(8, false)], "off8").unwrap() + }; + bin.builder.build_store(off8, action_name).unwrap(); + + // Write auth_count = 1 (offset 16, varuint32 for value 1 = single byte 0x01) + let off16 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(16, false)], "off16").unwrap() + }; + bin.builder + .build_store(off16, i8_ty.const_int(1, false)) + .unwrap(); + + // Write auth[0].actor = self (offset 17) + let off17 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(17, false)], "off17").unwrap() + }; + bin.builder.build_store(off17, self_account).unwrap(); + + // Write auth[0].permission = active (offset 25) + let off25 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(25, false)], "off25").unwrap() + }; + bin.builder.build_store(off25, active_perm).unwrap(); + + // Write data_len as varuint32 (offset 33) + let off33 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[fixed_part], "off33").unwrap() + }; + bin.builder + .build_call(encode_fn, &[off33.into(), data_len.into()], "") + .unwrap(); + + // Copy data after varuint32 + let data_offset = bin.builder.build_int_add(fixed_part, data_vi_size, "doff").unwrap(); + let data_dst = unsafe { + bin.builder.build_gep(i8_ty, buf, &[data_offset], "ddst").unwrap() + }; + bin.builder + .build_memcpy(data_dst, 1, data_ptr, 1, data_len) + .unwrap(); + + // Call send_inline(buf, total_size) + let send_inline_fn = bin.module.get_function("send_inline").unwrap(); + bin.builder + .build_call(send_inline_fn, &[buf.into(), total_size.into()], "") + .unwrap(); + + bin.context.i64_type().const_zero().into() + } + Expression::Builtin { + kind: Builtin::AntelopeCallAuth, + args, + .. + } => { + // antelope.callauth(contract, action_name, data, actor, permission) + let i32_ty = bin.context.i32_type(); + let i64_ty = bin.context.i64_type(); + let i8_ty = bin.context.i8_type(); + + let contract_account = crate::emit::expression::expression( + &AntelopeTarget, bin, &args[0], vartab, function, + ).into_int_value(); + let action_name = crate::emit::expression::expression( + &AntelopeTarget, bin, &args[1], vartab, function, + ).into_int_value(); + let data_vec = crate::emit::expression::expression( + &AntelopeTarget, bin, &args[2], vartab, function, + ); + let actor = crate::emit::expression::expression( + &AntelopeTarget, bin, &args[3], vartab, function, + ).into_int_value(); + let permission = crate::emit::expression::expression( + &AntelopeTarget, bin, &args[4], vartab, function, + ).into_int_value(); + + let data_len = bin.vector_len(data_vec); + let data_ptr = bin.vector_bytes(data_vec); + + let encode_fn = bin.module.get_function("__encode_varuint32").unwrap(); + let vi_tmp = bin.builder + .build_array_alloca(i8_ty, i32_ty.const_int(5, false), "vi_tmp") + .unwrap(); + let data_vi_size = bin.builder + .build_call(encode_fn, &[vi_tmp.into(), data_len.into()], "dvi_sz") + .unwrap().try_as_basic_value().left().unwrap().into_int_value(); + + let fixed_part = i32_ty.const_int(33, false); + let total_size = bin.builder.build_int_add(fixed_part, data_vi_size, "ts1").unwrap(); + let total_size = bin.builder.build_int_add(total_size, data_len, "total_size").unwrap(); + + let malloc_fn = bin.module.get_function("__malloc").unwrap(); + let buf = bin.builder + .build_call(malloc_fn, &[total_size.into()], "action_buf") + .unwrap().try_as_basic_value().left().unwrap().into_pointer_value(); + + // account (offset 0) + bin.builder.build_store(buf, contract_account).unwrap(); + // action_name (offset 8) + let off8 = unsafe { bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(8, false)], "off8").unwrap() }; + bin.builder.build_store(off8, action_name).unwrap(); + // auth_count = 1 (offset 16) + let off16 = unsafe { bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(16, false)], "off16").unwrap() }; + bin.builder.build_store(off16, i8_ty.const_int(1, false)).unwrap(); + // auth[0].actor (offset 17) + let off17 = unsafe { bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(17, false)], "off17").unwrap() }; + bin.builder.build_store(off17, actor).unwrap(); + // auth[0].permission (offset 25) + let off25 = unsafe { bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(25, false)], "off25").unwrap() }; + bin.builder.build_store(off25, permission).unwrap(); + // data_len varuint32 (offset 33) + let off33 = unsafe { bin.builder.build_gep(i8_ty, buf, &[fixed_part], "off33").unwrap() }; + bin.builder.build_call(encode_fn, &[off33.into(), data_len.into()], "").unwrap(); + // data bytes + let data_offset = bin.builder.build_int_add(fixed_part, data_vi_size, "doff").unwrap(); + let data_dst = unsafe { bin.builder.build_gep(i8_ty, buf, &[data_offset], "ddst").unwrap() }; + bin.builder.build_memcpy(data_dst, 1, data_ptr, 1, data_len).unwrap(); + + let send_inline_fn = bin.module.get_function("send_inline").unwrap(); + bin.builder.build_call(send_inline_fn, &[buf.into(), total_size.into()], "").unwrap(); + + bin.context.i64_type().const_zero().into() + } + Expression::Builtin { + kind: Builtin::AntelopeSetPayer, + args, + .. + } => { + let payer = crate::emit::expression::expression( + &AntelopeTarget, bin, &args[0], vartab, function, + ).into_int_value(); + + let payer_global = bin.module.get_global("__ram_payer").unwrap(); + bin.builder + .build_store(payer_global.as_pointer_value(), payer) + .unwrap(); + + bin.context.i64_type().const_zero().into() + } + Expression::Builtin { + kind: Builtin::AntelopeHasAuth, + args, + .. + } => { + let account = crate::emit::expression::expression( + &AntelopeTarget, bin, &args[0], vartab, function, + ).into_int_value(); + + let has_auth_fn = bin.module.get_function("has_auth").unwrap(); + let result = bin.builder + .build_call(has_auth_fn, &[account.into()], "has_auth_result") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // has_auth returns i32 (C bool); truncate to i1 for Solidity bool. + bin.builder + .build_int_truncate(result, bin.context.bool_type(), "has_auth_bool") + .unwrap() + .into() + } + Expression::Builtin { + kind: Builtin::AntelopeRequireAuth2, + args, + .. + } => { + let account = crate::emit::expression::expression( + &AntelopeTarget, bin, &args[0], vartab, function, + ).into_int_value(); + let permission = crate::emit::expression::expression( + &AntelopeTarget, bin, &args[1], vartab, function, + ).into_int_value(); + + let require_auth2_fn = bin.module.get_function("require_auth2").unwrap(); + bin.builder + .build_call(require_auth2_fn, &[account.into(), permission.into()], "") + .unwrap(); + + bin.context.i64_type().const_zero().into() + } + Expression::Builtin { + kind: Builtin::AntelopeTimestamp, + .. + } => { + let current_time_fn = bin.module.get_function("current_time").unwrap(); + bin.builder + .build_call(current_time_fn, &[], "timestamp") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + } + Expression::Builtin { + kind: Builtin::AntelopePack, + args, + .. + } => { + // antelope.pack(arg0, arg1, ...) → bytes memory + // Serialises each argument in Antelope CDT little-endian format: + // integers → N bytes stored directly (WASM is LE, so store is already LE) + // bool → 1 byte (0 or 1) + // string/bytes → varuint32(len) + raw bytes + // + // Steps: evaluate all args, compute total byte count, malloc, + // write each arg, return as a bytes vector. + + let i8_ty = bin.context.i8_type(); + let i32_ty = bin.context.i32_type(); + + let encode_fn = bin.module.get_function("__encode_varuint32").unwrap(); + let malloc_fn = bin.module.get_function("__malloc").unwrap(); + let vector_new_fn = bin.module.get_function("vector_new").unwrap(); + + // Peel through ZeroExt/SignExt wrappers to get the declared type. + // Storage-loaded variables are widened to Uint(256) by codegen, but + // for packing we want the original declared type (e.g. Uint(64)). + fn peel_type(e: &crate::codegen::Expression) -> crate::sema::ast::Type { + match e { + crate::codegen::Expression::ZeroExt { expr, .. } + | crate::codegen::Expression::SignExt { expr, .. } => peel_type(expr), + other => other.ty(), + } + } + + // Evaluate all args once and pair with their declared (peeled) types. + let arg_vals: Vec<(crate::sema::ast::Type, inkwell::values::BasicValueEnum)> = args + .iter() + .map(|a| { + let ty = peel_type(a); + let val = crate::emit::expression::expression( + &AntelopeTarget, bin, a, vartab, function, + ); + (ty, val) + }) + .collect(); + + // Pass 1: compute total packed byte count. + // Fixed-size widths come from the SHARED datastream_fixed_size (same + // authority as the deserializer), so pack can never drift from it. + // None = variable-length (string/bytes): varuint32(len) + raw bytes. + let mut total = i32_ty.const_int(0, false); + for (ty, val) in &arg_vals { + let fixed: Option = + AntelopeTarget::datastream_fixed_size(ty, bin).map(|n| n as u64); + if let Some(n) = fixed { + total = bin.builder.build_int_add(total, i32_ty.const_int(n, false), "").unwrap(); + } else { + // string/bytes: varuint32(len) + len bytes + let data_len = bin.vector_len(*val); + let scratch = bin.builder.build_array_alloca(i8_ty, i32_ty.const_int(5, false), "vi_sc").unwrap(); + let vi_sz = bin.builder + .build_call(encode_fn, &[scratch.into(), data_len.into()], "vi_sz") + .unwrap().try_as_basic_value().left().unwrap().into_int_value(); + total = bin.builder.build_int_add(total, vi_sz, "").unwrap(); + total = bin.builder.build_int_add(total, data_len, "").unwrap(); + } + } + + // Allocate output buffer. + let buf = bin.builder + .build_call(malloc_fn, &[total.into()], "pack_buf") + .unwrap().try_as_basic_value().left().unwrap().into_pointer_value(); + + // Pass 2: write each arg into buf. + let mut offset = i32_ty.const_int(0, false); + for (ty, val) in &arg_vals { + macro_rules! write_int { + ($nbytes:expr) => {{ + let ptr = unsafe { + bin.builder.build_gep(i8_ty, buf, &[offset], "iptr").unwrap() + }; + let int_val = val.into_int_value(); + let target_bits = ($nbytes as u32) * 8; + let actual_bits = int_val.get_type().get_bit_width(); + if actual_bits > target_bits { + let trunc = bin.builder.build_int_truncate( + int_val, + bin.context.custom_width_int_type(target_bits), + "pack_trunc", + ).unwrap(); + bin.builder.build_store(ptr, trunc).unwrap(); + } else { + bin.builder.build_store(ptr, int_val).unwrap(); + }; + offset = bin.builder.build_int_add( + offset, i32_ty.const_int($nbytes, false), "" + ).unwrap(); + }}; + } + match ty { + crate::sema::ast::Type::Bool => { + let byte_val = bin.builder + .build_int_z_extend((*val).into_int_value(), i8_ty, "boolbyte") + .unwrap(); + let ptr = unsafe { + bin.builder.build_gep(i8_ty, buf, &[offset], "bptr").unwrap() + }; + bin.builder.build_store(ptr, byte_val).unwrap(); + offset = bin.builder.build_int_add(offset, i32_ty.const_int(1, false), "").unwrap(); + } + crate::sema::ast::Type::String | crate::sema::ast::Type::DynamicBytes => { + // Write varuint32(len) then copy bytes. + let data_len = bin.vector_len(*val); + let data_ptr = bin.vector_bytes(*val); + let vi_dst = unsafe { + bin.builder.build_gep(i8_ty, buf, &[offset], "vidst").unwrap() + }; + let vi_sz = bin.builder + .build_call(encode_fn, &[vi_dst.into(), data_len.into()], "vi_sz2") + .unwrap().try_as_basic_value().left().unwrap().into_int_value(); + let data_off = bin.builder.build_int_add(offset, vi_sz, "").unwrap(); + let data_dst = unsafe { + bin.builder.build_gep(i8_ty, buf, &[data_off], "ddst").unwrap() + }; + bin.builder.build_memcpy(data_dst, 1, data_ptr, 1, data_len).unwrap(); + let field_total = bin.builder.build_int_add(vi_sz, data_len, "").unwrap(); + offset = bin.builder.build_int_add(offset, field_total, "").unwrap(); + } + // Every other supported type is fixed-size (int/uint of any + // width, bytesN, address, enum, value): write its N raw + // little-endian bytes. Size from the shared authority. + _ => { + let n = AntelopeTarget::datastream_fixed_size(ty, bin) + .expect("antelope.pack: variable-length type in fixed branch") as u64; + write_int!(n); + } + } + } + + // Return as bytes vector: vector_new(total, 1, buf). + bin.builder + .build_call(vector_new_fn, &[total.into(), i32_ty.const_int(1, false).into(), buf.into()], "packed_vec") + .unwrap().try_as_basic_value().left().unwrap() + } + // ===== Decode helpers ===== + // antelope.toUint64(bytes, offset), toInt64, toUint32, toUint128, toBytes32 + Expression::Builtin { + kind: kind @ (Builtin::AntelopeToUint64 + | Builtin::AntelopeToInt64 + | Builtin::AntelopeToUint32 + | Builtin::AntelopeToUint128 + | Builtin::AntelopeToBytes32), + args, + .. + } => { + let i32_ty = bin.context.i32_type(); + let data = crate::emit::expression::expression(&AntelopeTarget, bin, &args[0], vartab, function); + let offset = crate::emit::expression::expression(&AntelopeTarget, bin, &args[1], vartab, function).into_int_value(); + let data_ptr = bin.vector_bytes(data); + + // GEP to data_ptr + offset + let elem_ptr = unsafe { + bin.builder.build_gep( + bin.context.i8_type(), + data_ptr, + &[offset], + "elem_ptr", + ).unwrap() + }; + + match kind { + Builtin::AntelopeToUint64 | Builtin::AntelopeToInt64 => { + bin.builder.build_load(bin.context.i64_type(), elem_ptr, "decoded").unwrap() + } + Builtin::AntelopeToUint32 => { + bin.builder.build_load(i32_ty, elem_ptr, "decoded").unwrap() + } + Builtin::AntelopeToUint128 => { + bin.builder.build_load(bin.context.custom_width_int_type(128), elem_ptr, "decoded").unwrap() + } + Builtin::AntelopeToBytes32 => { + bin.builder.build_load(bin.context.custom_width_int_type(256), elem_ptr, "decoded").unwrap() + } + _ => unreachable!(), + } + } + + // antelope.toString(bytes, offset) + Expression::Builtin { + kind: Builtin::AntelopeToString, + args, + .. + } => { + let i32_ty = bin.context.i32_type(); + let i64_ty = bin.context.i64_type(); + + let data = crate::emit::expression::expression(&AntelopeTarget, bin, &args[0], vartab, function); + let offset = crate::emit::expression::expression(&AntelopeTarget, bin, &args[1], vartab, function).into_int_value(); + let data_ptr = bin.vector_bytes(data); + + // Pointer to the varuint32 at offset + let varuint_ptr = unsafe { + bin.builder.build_gep(bin.context.i8_type(), data_ptr, &[offset], "varuint_ptr").unwrap() + }; + + // Decode varuint32: returns packed u64 (low32=value, high32=bytes_read) + let decode_fn = bin.module.get_function("__decode_varuint32").unwrap(); + let packed = bin.builder + .build_call(decode_fn, &[varuint_ptr.into()], "packed") + .unwrap().try_as_basic_value().left().unwrap().into_int_value(); + + let str_len_i64 = bin.builder + .build_and(packed, i64_ty.const_int(0xFFFF_FFFF, false), "str_len_64").unwrap(); + let str_len = bin.builder + .build_int_truncate(str_len_i64, i32_ty, "str_len").unwrap(); + let bytes_read_i64 = bin.builder + .build_right_shift(packed, i64_ty.const_int(32, false), false, "br_64").unwrap(); + let bytes_read = bin.builder + .build_int_truncate(bytes_read_i64, i32_ty, "bytes_read").unwrap(); + + // String data starts at offset + bytes_read + let after_len = bin.builder.build_int_add(offset, bytes_read, "after_len").unwrap(); + let str_data_ptr = unsafe { + bin.builder.build_gep(bin.context.i8_type(), data_ptr, &[after_len], "str_data").unwrap() + }; + + // Create a vector (string) from the data + let vector_new_fn = bin.module.get_function("vector_new").unwrap(); + bin.builder + .build_call(vector_new_fn, &[str_len.into(), i32_ty.const_int(1, false).into(), str_data_ptr.into()], "str_vec") + .unwrap().try_as_basic_value().left().unwrap() + } + + // ===== Table read builtins ===== + + // antelope.dbFind(code, scope, table, pk) -> int32 iterator + Expression::Builtin { + kind: Builtin::AntelopeDbFind, + args, + .. + } => { + let code = crate::emit::expression::expression(&AntelopeTarget, bin, &args[0], vartab, function).into_int_value(); + let scope = crate::emit::expression::expression(&AntelopeTarget, bin, &args[1], vartab, function).into_int_value(); + let table = crate::emit::expression::expression(&AntelopeTarget, bin, &args[2], vartab, function).into_int_value(); + let pk = crate::emit::expression::expression(&AntelopeTarget, bin, &args[3], vartab, function).into_int_value(); + + let db_find_fn = bin.module.get_function("db_find_i64").unwrap(); + bin.builder + .build_call(db_find_fn, &[code.into(), scope.into(), table.into(), pk.into()], "db_find") + .unwrap().try_as_basic_value().left().unwrap() + } + + // antelope.dbLowerbound(code, scope, table, id) -> int32 iterator + Expression::Builtin { + kind: Builtin::AntelopeDbLowerbound, + args, + .. + } => { + let code = crate::emit::expression::expression(&AntelopeTarget, bin, &args[0], vartab, function).into_int_value(); + let scope = crate::emit::expression::expression(&AntelopeTarget, bin, &args[1], vartab, function).into_int_value(); + let table = crate::emit::expression::expression(&AntelopeTarget, bin, &args[2], vartab, function).into_int_value(); + let id = crate::emit::expression::expression(&AntelopeTarget, bin, &args[3], vartab, function).into_int_value(); + + let db_lb_fn = bin.module.get_function("db_lowerbound_i64").unwrap(); + bin.builder + .build_call(db_lb_fn, &[code.into(), scope.into(), table.into(), id.into()], "db_lb") + .unwrap().try_as_basic_value().left().unwrap() + } + + // antelope.dbGet(iterator) -> bytes + Expression::Builtin { + kind: Builtin::AntelopeDbGet, + args, + .. + } => { + let i32_ty = bin.context.i32_type(); + let iterator = crate::emit::expression::expression(&AntelopeTarget, bin, &args[0], vartab, function).into_int_value(); + + let db_get_fn = bin.module.get_function("db_get_i64").unwrap(); + let ptr_ty = bin.context.ptr_type(inkwell::AddressSpace::default()); + + // First call: get data size (pass null ptr, len 0) + let data_size = bin.builder + .build_call(db_get_fn, &[iterator.into(), ptr_ty.const_null().into(), i32_ty.const_zero().into()], "data_size") + .unwrap().try_as_basic_value().left().unwrap().into_int_value(); + + // Allocate buffer via __malloc + let malloc_fn = bin.module.get_function("__malloc").unwrap(); + let buf = bin.builder + .build_call(malloc_fn, &[data_size.into()], "buf") + .unwrap().try_as_basic_value().left().unwrap().into_pointer_value(); + + // Second call: read data into buffer + bin.builder + .build_call(db_get_fn, &[iterator.into(), buf.into(), data_size.into()], "") + .unwrap(); + + // Wrap as bytes vector + let vector_new_fn = bin.module.get_function("vector_new").unwrap(); + bin.builder + .build_call(vector_new_fn, &[data_size.into(), i32_ty.const_int(1, false).into(), buf.into()], "row_bytes") + .unwrap().try_as_basic_value().left().unwrap() + } + + // antelope.dbNext(iterator) -> int32 new_iterator; caches pk in __last_pk + Expression::Builtin { + kind: Builtin::AntelopeDbNext, + args, + .. + } => { + let i64_ty = bin.context.i64_type(); + let iterator = crate::emit::expression::expression(&AntelopeTarget, bin, &args[0], vartab, function).into_int_value(); + + // Alloca for pk output + let pk_out = bin.builder.build_alloca(i64_ty, "pk_out").unwrap(); + + let db_next_fn = bin.module.get_function("db_next_i64").unwrap(); + let next_iter = bin.builder + .build_call(db_next_fn, &[iterator.into(), pk_out.into()], "next_iter") + .unwrap().try_as_basic_value().left().unwrap().into_int_value(); + + // Cache pk in __last_pk global + let pk_val = bin.builder.build_load(i64_ty, pk_out, "pk_val").unwrap().into_int_value(); + let last_pk_global = bin.module.get_global("__last_pk").unwrap().as_pointer_value(); + bin.builder.build_store(last_pk_global, pk_val).unwrap(); + + next_iter.into() + } + + // antelope.lastPk() -> uint64 + Expression::Builtin { + kind: Builtin::AntelopeLastPk, + .. + } => { + let i64_ty = bin.context.i64_type(); + let last_pk_global = bin.module.get_global("__last_pk").unwrap().as_pointer_value(); + bin.builder.build_load(i64_ty, last_pk_global, "last_pk").unwrap() + } + + // ===== Secondary index builtins ===== + // All share the same pattern: compute secondary table name, call host, cache pk + + // antelope.dbIdx64Find(code, scope, table, indexNum, key) -> int32 + // antelope.dbIdx64Lowerbound(code, scope, table, indexNum, key) -> int32 + Expression::Builtin { + kind: kind @ (Builtin::AntelopeDbIdx64Find | Builtin::AntelopeDbIdx64Lowerbound), + args, + .. + } => { + let i64_ty = bin.context.i64_type(); + let code = crate::emit::expression::expression(&AntelopeTarget, bin, &args[0], vartab, function).into_int_value(); + let scope = crate::emit::expression::expression(&AntelopeTarget, bin, &args[1], vartab, function).into_int_value(); + let table = crate::emit::expression::expression(&AntelopeTarget, bin, &args[2], vartab, function).into_int_value(); + let index_num = crate::emit::expression::expression(&AntelopeTarget, bin, &args[3], vartab, function).into_int_value(); + let key = crate::emit::expression::expression(&AntelopeTarget, bin, &args[4], vartab, function).into_int_value(); + + // Compute secondary table name: (table & 0xFFFFFFFFFFFFFFF0) | (indexNum & 0xF) + let mask = i64_ty.const_int(0xFFFFFFFFFFFFFFF0, false); + let table_masked = bin.builder.build_and(table, mask, "tbl_masked").unwrap(); + let idx_mask = i64_ty.const_int(0xF, false); + let idx_masked = bin.builder.build_and(index_num, idx_mask, "idx_masked").unwrap(); + let sec_table = bin.builder.build_or(table_masked, idx_masked, "sec_table").unwrap(); + + // Alloca for key and pk output + let key_ptr = bin.builder.build_alloca(i64_ty, "sec_key").unwrap(); + bin.builder.build_store(key_ptr, key).unwrap(); + let pk_out = bin.builder.build_alloca(i64_ty, "pk_out").unwrap(); + + let host_fn_name = match kind { + Builtin::AntelopeDbIdx64Find => "db_idx64_find_secondary", + Builtin::AntelopeDbIdx64Lowerbound => "db_idx64_lowerbound", + _ => unreachable!(), + }; + let host_fn = bin.module.get_function(host_fn_name).unwrap(); + let iter = bin.builder + .build_call(host_fn, &[code.into(), scope.into(), sec_table.into(), key_ptr.into(), pk_out.into()], "idx64_iter") + .unwrap().try_as_basic_value().left().unwrap().into_int_value(); + + // Cache pk + let pk_val = bin.builder.build_load(i64_ty, pk_out, "pk_val").unwrap().into_int_value(); + let last_pk_global = bin.module.get_global("__last_pk").unwrap().as_pointer_value(); + bin.builder.build_store(last_pk_global, pk_val).unwrap(); + + iter.into() + } + + // antelope.dbIdx128Find(code, scope, table, indexNum, key) -> int32 + // antelope.dbIdx128Lowerbound(code, scope, table, indexNum, key) -> int32 + Expression::Builtin { + kind: kind @ (Builtin::AntelopeDbIdx128Find | Builtin::AntelopeDbIdx128Lowerbound), + args, + .. + } => { + let i64_ty = bin.context.i64_type(); + let i128_ty = bin.context.custom_width_int_type(128); + let code = crate::emit::expression::expression(&AntelopeTarget, bin, &args[0], vartab, function).into_int_value(); + let scope = crate::emit::expression::expression(&AntelopeTarget, bin, &args[1], vartab, function).into_int_value(); + let table = crate::emit::expression::expression(&AntelopeTarget, bin, &args[2], vartab, function).into_int_value(); + let index_num = crate::emit::expression::expression(&AntelopeTarget, bin, &args[3], vartab, function).into_int_value(); + let key = crate::emit::expression::expression(&AntelopeTarget, bin, &args[4], vartab, function).into_int_value(); + + // Compute secondary table name + let mask = i64_ty.const_int(0xFFFFFFFFFFFFFFF0, false); + let table_masked = bin.builder.build_and(table, mask, "tbl_masked").unwrap(); + let idx_mask = i64_ty.const_int(0xF, false); + let idx_masked = bin.builder.build_and(index_num, idx_mask, "idx_masked").unwrap(); + let sec_table = bin.builder.build_or(table_masked, idx_masked, "sec_table").unwrap(); + + // Alloca for 128-bit key and pk output + let key_ptr = bin.builder.build_alloca(i128_ty, "sec_key128").unwrap(); + bin.builder.build_store(key_ptr, key).unwrap(); + let pk_out = bin.builder.build_alloca(i64_ty, "pk_out").unwrap(); + + let host_fn_name = match kind { + Builtin::AntelopeDbIdx128Find => "db_idx128_find_secondary", + Builtin::AntelopeDbIdx128Lowerbound => "db_idx128_lowerbound", + _ => unreachable!(), + }; + let host_fn = bin.module.get_function(host_fn_name).unwrap(); + let iter = bin.builder + .build_call(host_fn, &[code.into(), scope.into(), sec_table.into(), key_ptr.into(), pk_out.into()], "idx128_iter") + .unwrap().try_as_basic_value().left().unwrap().into_int_value(); + + // Cache pk + let pk_val = bin.builder.build_load(i64_ty, pk_out, "pk_val").unwrap().into_int_value(); + let last_pk_global = bin.module.get_global("__last_pk").unwrap().as_pointer_value(); + bin.builder.build_store(last_pk_global, pk_val).unwrap(); + + iter.into() + } + + // antelope.dbIdx256Find(code, scope, table, indexNum, key) -> int32 + // antelope.dbIdx256Lowerbound(code, scope, table, indexNum, key) -> int32 + Expression::Builtin { + kind: kind @ (Builtin::AntelopeDbIdx256Find | Builtin::AntelopeDbIdx256Lowerbound), + args, + .. + } => { + let i32_ty = bin.context.i32_type(); + let i64_ty = bin.context.i64_type(); + let i256_ty = bin.context.custom_width_int_type(256); + let code = crate::emit::expression::expression(&AntelopeTarget, bin, &args[0], vartab, function).into_int_value(); + let scope = crate::emit::expression::expression(&AntelopeTarget, bin, &args[1], vartab, function).into_int_value(); + let table = crate::emit::expression::expression(&AntelopeTarget, bin, &args[2], vartab, function).into_int_value(); + let index_num = crate::emit::expression::expression(&AntelopeTarget, bin, &args[3], vartab, function).into_int_value(); + let key = crate::emit::expression::expression(&AntelopeTarget, bin, &args[4], vartab, function).into_int_value(); + + // Compute secondary table name + let mask = i64_ty.const_int(0xFFFFFFFFFFFFFFF0, false); + let table_masked = bin.builder.build_and(table, mask, "tbl_masked").unwrap(); + let idx_mask = i64_ty.const_int(0xF, false); + let idx_masked = bin.builder.build_and(index_num, idx_mask, "idx_masked").unwrap(); + let sec_table = bin.builder.build_or(table_masked, idx_masked, "sec_table").unwrap(); + + // Alloca for 256-bit key and pk output + let key_ptr = bin.builder.build_alloca(i256_ty, "sec_key256").unwrap(); + bin.builder.build_store(key_ptr, key).unwrap(); + let pk_out = bin.builder.build_alloca(i64_ty, "pk_out").unwrap(); + + let host_fn_name = match kind { + Builtin::AntelopeDbIdx256Find => "db_idx256_find_secondary", + Builtin::AntelopeDbIdx256Lowerbound => "db_idx256_lowerbound", + _ => unreachable!(), + }; + let host_fn = bin.module.get_function(host_fn_name).unwrap(); + // idx256 host functions take data_len = 2 (number of 128-bit words) + let iter = bin.builder + .build_call(host_fn, &[code.into(), scope.into(), sec_table.into(), key_ptr.into(), i32_ty.const_int(2, false).into(), pk_out.into()], "idx256_iter") + .unwrap().try_as_basic_value().left().unwrap().into_int_value(); + + // Cache pk + let pk_val = bin.builder.build_load(i64_ty, pk_out, "pk_val").unwrap().into_int_value(); + let last_pk_global = bin.module.get_global("__last_pk").unwrap().as_pointer_value(); + bin.builder.build_store(last_pk_global, pk_val).unwrap(); + + iter.into() + } + + _ => panic!("antelope: unimplemented builtin expression: {expr:?}"), + } + } + + fn return_data<'b>(&self, bin: &Binary<'b>, function: FunctionValue<'b>) -> PointerValue<'b> { + todo!("antelope: return_data") + } + + fn value_transferred<'b>(&self, bin: &Binary<'b>) -> IntValue<'b> { + unimplemented!("antelope: value_transferred not supported") + } + + fn selfdestruct<'b>(&self, bin: &Binary<'b>, addr: ArrayValue<'b>) { + unimplemented!("antelope: selfdestruct not supported") + } + + fn hash<'b>( + &self, + bin: &Binary<'b>, + function: FunctionValue<'b>, + hash: HashTy, + string: PointerValue<'b>, + length: IntValue<'b>, + ) -> IntValue<'b> { + todo!("antelope: hash") + } + + fn emit_event<'b>( + &self, + bin: &Binary<'b>, + function: FunctionValue<'b>, + data: BasicValueEnum<'b>, + topics: &[BasicValueEnum<'b>], + ) { + // Antelope events are emitted as inline actions to self via send_inline. + // + // topics[0] = eosio::name(event_name) as uint64 (compile-time constant) + // data = ABI-encoded event fields (vector of bytes) + // + // Serialized action layout: + // account(8) + action_name(8) + auth_count(1) + actor(8) + perm(8) + // + data_len(varuint32) + data(N) + + let i32_ty = bin.context.i32_type(); + let i64_ty = bin.context.i64_type(); + let i8_ty = bin.context.i8_type(); + + // topics[0] is the event name as eosio::name uint64. + let event_name = if !topics.is_empty() { + topics[0].into_int_value() + } else { + i64_ty.const_zero() + }; + + // Get data length and pointer from the ABI-encoded vector. + let data_len = bin.vector_len(data); + let data_ptr = bin.vector_bytes(data); + + // Load self account. + let receiver_global = AntelopeTarget::get_receiver_global(bin); + let self_account = bin + .builder + .build_load(i64_ty, receiver_global.as_pointer_value(), "self_acct") + .unwrap() + .into_int_value(); + + let active_perm = i64_ty.const_int(crate::emit::antelope::string_to_name("active"), false); + + // Encode data_len as varuint32 to know its byte size. + let encode_fn = bin.module.get_function("__encode_varuint32").unwrap(); + let vi_tmp = bin + .builder + .build_array_alloca(i8_ty, i32_ty.const_int(5, false), "vi_tmp") + .unwrap(); + let data_vi_size = bin + .builder + .build_call(encode_fn, &[vi_tmp.into(), data_len.into()], "dvi_sz") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // Total = 33 (fixed) + varuint_size + data_len + let fixed_part = i32_ty.const_int(33, false); + let total_size = bin.builder.build_int_add(fixed_part, data_vi_size, "ts1").unwrap(); + let total_size = bin.builder.build_int_add(total_size, data_len, "total_size").unwrap(); + + let malloc_fn = bin.module.get_function("__malloc").unwrap(); + let buf = bin + .builder + .build_call(malloc_fn, &[total_size.into()], "evt_buf") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_pointer_value(); + + // account = self (offset 0) + bin.builder.build_store(buf, self_account).unwrap(); + + // action_name = event name (offset 8) + let off8 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(8, false)], "off8").unwrap() + }; + bin.builder.build_store(off8, event_name).unwrap(); + + // auth_count = 1 (offset 16) + let off16 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(16, false)], "off16").unwrap() + }; + bin.builder.build_store(off16, i8_ty.const_int(1, false)).unwrap(); + + // auth[0].actor = self (offset 17) + let off17 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(17, false)], "off17").unwrap() + }; + bin.builder.build_store(off17, self_account).unwrap(); + + // auth[0].permission = active (offset 25) + let off25 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[i32_ty.const_int(25, false)], "off25").unwrap() + }; + bin.builder.build_store(off25, active_perm).unwrap(); + + // data_len as varuint32 (offset 33) + let off33 = unsafe { + bin.builder.build_gep(i8_ty, buf, &[fixed_part], "off33").unwrap() + }; + bin.builder + .build_call(encode_fn, &[off33.into(), data_len.into()], "") + .unwrap(); + + // data bytes (offset 33 + varuint_size) + let data_offset = bin.builder.build_int_add(fixed_part, data_vi_size, "doff").unwrap(); + let data_dst = unsafe { + bin.builder.build_gep(i8_ty, buf, &[data_offset], "ddst").unwrap() + }; + bin.builder + .build_memcpy(data_dst, 1, data_ptr, 1, data_len) + .unwrap(); + + // send_inline(buf, total_size) + let send_inline_fn = bin.module.get_function("send_inline").unwrap(); + bin.builder + .build_call(send_inline_fn, &[buf.into(), total_size.into()], "") + .unwrap(); + } + + fn return_abi_data<'b>( + &self, + bin: &Binary<'b>, + data: PointerValue<'b>, + data_len: BasicValueEnum<'b>, + ) { + let set_return = bin.module.get_function("set_action_return_value").unwrap(); + bin.builder + .build_call(set_return, &[data.into(), data_len.into()], "") + .unwrap(); + } +} + +/// Helper methods for Antelope string/variable-length storage. +impl AntelopeTarget { + /// Store a string (Solang vector) to the state table. + /// Row format: [pk(8) + slot_hash(32) + string_bytes(N)]. + fn storage_store_string<'a>( + &self, + bin: &Binary<'a>, + slot: &mut IntValue<'a>, + dest: BasicValueEnum<'a>, + function: FunctionValue, + ) { + let i32_ty = bin.context.i32_type(); + let i64_ty = bin.context.i64_type(); + let i256_ty = bin.context.custom_width_int_type(256); + + // Get string data pointer and length from Solang vector. + let string_len = bin.vector_len(dest); + let string_data = bin.vector_bytes(dest); + + // Load receiver. + let receiver_global = Self::get_receiver_global(bin); + let receiver = bin + .builder + .build_load(i64_ty, receiver_global.as_pointer_value(), "receiver") + .unwrap() + .into_int_value(); + let ram_payer = Self::get_ram_payer(bin); + let table_name = i64_ty.const_int(STATE_TABLE_NAME, false); + + // Convert slot to 256-bit. + let slot_i256 = if slot.get_type().get_bit_width() == 256 { + *slot + } else { + bin.builder + .build_int_z_extend(*slot, i256_ty, "slot256") + .unwrap() + }; + let slot_buf = bin + .builder + .build_array_alloca(bin.context.i8_type(), i32_ty.const_int(32, false), "slot_buf") + .unwrap(); + bin.builder.build_store(slot_buf, slot_i256).unwrap(); + + // Encode string_len as varuint32 into a temp buffer to know how many bytes it takes. + let encode_fn = bin.module.get_function("__encode_varuint32").unwrap(); + let varuint_tmp = bin + .builder + .build_array_alloca(bin.context.i8_type(), i32_ty.const_int(5, false), "vi_tmp") + .unwrap(); + let varuint_size = bin + .builder + .build_call(encode_fn, &[varuint_tmp.into(), string_len.into()], "vi_sz") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // Row size = 8 (pk) + 32 (hash) + varuint_size + string_len. + let fixed_header = i32_ty.const_int(40, false); // pk(8) + hash(32) + let row_size = bin.builder.build_int_add(fixed_header, varuint_size, "rs1").unwrap(); + let row_size = bin.builder.build_int_add(row_size, string_len, "row_size").unwrap(); + + // Allocate row buffer via malloc (dynamic size, can't use stack alloca). + let malloc = bin.module.get_function("__malloc").unwrap(); + let row_buf = bin + .builder + .build_call(malloc, &[row_size.into()], "row_buf") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_pointer_value(); + + // Write slot_hash at offset 8. + let hash_ptr = unsafe { + bin.builder + .build_gep(bin.context.i8_type(), row_buf, &[i32_ty.const_int(8, false)], "hash_ptr") + .unwrap() + }; + bin.builder.build_store(hash_ptr, slot_i256).unwrap(); + + // Write varuint32(string_len) at offset 40 using __encode_varuint32. + let varuint_ptr = unsafe { + bin.builder + .build_gep(bin.context.i8_type(), row_buf, &[fixed_header], "vi_ptr") + .unwrap() + }; + bin.builder + .build_call(encode_fn, &[varuint_ptr.into(), string_len.into()], "") + .unwrap(); + + // Copy string bytes after the varuint32. + let data_offset = bin.builder.build_int_add(fixed_header, varuint_size, "doff").unwrap(); + let val_ptr = unsafe { + bin.builder + .build_gep(bin.context.i8_type(), row_buf, &[data_offset], "val_ptr") + .unwrap() + }; + let memcpy = bin.module.get_function("__memcpy").unwrap(); + bin.builder + .build_call(memcpy, &[val_ptr.into(), string_data.into(), string_len.into()], "") + .unwrap(); + + let data_len = i32_ty.const_int(2, false); // idx256 data_len + + // Look up existing row via idx256. + let pk_out = bin.builder.build_alloca(i64_ty, "pk_out").unwrap(); + let db_idx256_find = bin.module.get_function("db_idx256_find_secondary").unwrap(); + let sec_iter = bin + .builder + .build_call( + db_idx256_find, + &[receiver.into(), receiver.into(), table_name.into(), slot_buf.into(), data_len.into(), pk_out.into()], + "sec_iter", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let found = bin + .builder + .build_int_compare(IntPredicate::SGE, sec_iter, i32_ty.const_zero(), "found") + .unwrap(); + + let update_bb = bin.context.append_basic_block(function, "str_update"); + let insert_bb = bin.context.append_basic_block(function, "str_insert"); + let done_bb = bin.context.append_basic_block(function, "str_done"); + + bin.builder.build_conditional_branch(found, update_bb, insert_bb).unwrap(); + + // UPDATE: write pk, find primary, update row. + bin.builder.position_at_end(update_bb); + let pk = bin.builder.build_load(i64_ty, pk_out, "pk").unwrap().into_int_value(); + bin.builder.build_store(row_buf, pk).unwrap(); + + let db_find = bin.module.get_function("db_find_i64").unwrap(); + let pri_iter = bin + .builder + .build_call(db_find, &[receiver.into(), receiver.into(), table_name.into(), pk.into()], "pri_iter") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + let db_update = bin.module.get_function("db_update_i64").unwrap(); + bin.builder + .build_call(db_update, &[pri_iter.into(), ram_payer.into(), row_buf.into(), row_size.into()], "") + .unwrap(); + bin.builder.build_unconditional_branch(done_bb).unwrap(); + + // INSERT: allocate new pk, store row + idx256. + bin.builder.position_at_end(insert_bb); + let pk_global = Self::get_next_pk_global(bin); + let cached_pk = bin + .builder + .build_load(i64_ty, pk_global.as_pointer_value(), "cached_pk") + .unwrap() + .into_int_value(); + + let sentinel = i64_ty.const_all_ones(); + let need_init = bin + .builder + .build_int_compare(IntPredicate::EQ, cached_pk, sentinel, "need_init") + .unwrap(); + + let init_bb = bin.context.append_basic_block(function, "str_pk_init"); + let use_cached_bb = bin.context.append_basic_block(function, "str_pk_cached"); + let do_insert_bb = bin.context.append_basic_block(function, "str_do_insert"); + + bin.builder.build_conditional_branch(need_init, init_bb, use_cached_bb).unwrap(); + + // INIT pk from DB. + bin.builder.position_at_end(init_bb); + let db_end = bin.module.get_function("db_end_i64").unwrap(); + let end_iter = bin + .builder + .build_call(db_end, &[receiver.into(), receiver.into(), table_name.into()], "end_iter") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let end_neg = bin + .builder + .build_int_compare(IntPredicate::EQ, end_iter, i32_ty.const_int(u64::MAX, true), "end_neg") + .unwrap(); + + let empty_bb = bin.context.append_basic_block(function, "str_empty"); + let has_rows_bb = bin.context.append_basic_block(function, "str_has_rows"); + let init_done_bb = bin.context.append_basic_block(function, "str_init_done"); + + bin.builder.build_conditional_branch(end_neg, empty_bb, has_rows_bb).unwrap(); + + bin.builder.position_at_end(empty_bb); + let pk_zero = i64_ty.const_zero(); + bin.builder.build_unconditional_branch(init_done_bb).unwrap(); + + bin.builder.position_at_end(has_rows_bb); + let last_pk_out = bin.builder.build_alloca(i64_ty, "last_pk_out").unwrap(); + let db_previous = bin.module.get_function("db_previous_i64").unwrap(); + bin.builder.build_call(db_previous, &[end_iter.into(), last_pk_out.into()], "").unwrap(); + let last_pk = bin.builder.build_load(i64_ty, last_pk_out, "last_pk").unwrap().into_int_value(); + let pk_from_db = bin.builder.build_int_add(last_pk, i64_ty.const_int(1, false), "pk_from_db").unwrap(); + bin.builder.build_unconditional_branch(init_done_bb).unwrap(); + + bin.builder.position_at_end(init_done_bb); + let init_pk = bin.builder.build_phi(i64_ty, "init_pk").unwrap(); + init_pk.add_incoming(&[(&pk_zero, empty_bb), (&pk_from_db, has_rows_bb)]); + let init_pk_val = init_pk.as_basic_value().into_int_value(); + bin.builder.build_unconditional_branch(do_insert_bb).unwrap(); + + bin.builder.position_at_end(use_cached_bb); + bin.builder.build_unconditional_branch(do_insert_bb).unwrap(); + + bin.builder.position_at_end(do_insert_bb); + let new_pk = bin.builder.build_phi(i64_ty, "new_pk").unwrap(); + new_pk.add_incoming(&[(&init_pk_val, init_done_bb), (&cached_pk, use_cached_bb)]); + let new_pk_val = new_pk.as_basic_value().into_int_value(); + + bin.builder.build_store(row_buf, new_pk_val).unwrap(); + + let next_pk_inc = bin + .builder + .build_int_add(new_pk_val, i64_ty.const_int(1, false), "next_pk_inc") + .unwrap(); + bin.builder.build_store(pk_global.as_pointer_value(), next_pk_inc).unwrap(); + + let db_store = bin.module.get_function("db_store_i64").unwrap(); + bin.builder + .build_call( + db_store, + &[receiver.into(), table_name.into(), ram_payer.into(), new_pk_val.into(), row_buf.into(), row_size.into()], + "", + ) + .unwrap(); + + let db_idx256_store = bin.module.get_function("db_idx256_store").unwrap(); + bin.builder + .build_call( + db_idx256_store, + &[receiver.into(), table_name.into(), ram_payer.into(), new_pk_val.into(), slot_buf.into(), data_len.into()], + "", + ) + .unwrap(); + + bin.builder.build_unconditional_branch(done_bb).unwrap(); + bin.builder.position_at_end(done_bb); + } + + /// Load a string from the state table into a Solang vector. + /// Row format: [pk(8) + slot_hash(32) + string_bytes(N)]. + /// Returns a pointer to a new vector (or empty vector if not found). + fn storage_load_string<'a>( + &self, + bin: &Binary<'a>, + slot: &mut IntValue<'a>, + function: FunctionValue, + ) -> PointerValue<'a> { + let i32_ty = bin.context.i32_type(); + let i64_ty = bin.context.i64_type(); + let i256_ty = bin.context.custom_width_int_type(256); + + // Load receiver. + let receiver_global = Self::get_receiver_global(bin); + let receiver = bin + .builder + .build_load(i64_ty, receiver_global.as_pointer_value(), "receiver") + .unwrap() + .into_int_value(); + let table_name = i64_ty.const_int(STATE_TABLE_NAME, false); + + // Convert slot to 256-bit. + let slot_i256 = if slot.get_type().get_bit_width() == 256 { + *slot + } else { + bin.builder + .build_int_z_extend(*slot, i256_ty, "slot256") + .unwrap() + }; + let slot_buf = bin + .builder + .build_array_alloca(bin.context.i8_type(), i32_ty.const_int(32, false), "slot_buf") + .unwrap(); + bin.builder.build_store(slot_buf, slot_i256).unwrap(); + + // Look up via idx256. + let pk_out = bin.builder.build_alloca(i64_ty, "pk_out").unwrap(); + let db_idx256_find = bin.module.get_function("db_idx256_find_secondary").unwrap(); + let data_len = i32_ty.const_int(2, false); + let sec_iter = bin + .builder + .build_call( + db_idx256_find, + &[receiver.into(), receiver.into(), table_name.into(), slot_buf.into(), data_len.into(), pk_out.into()], + "sec_iter", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let found = bin + .builder + .build_int_compare(IntPredicate::SGE, sec_iter, i32_ty.const_zero(), "found") + .unwrap(); + + // Allocate scratch buffer before branching (must dominate both paths). + let scratch_buf = bin.builder.build_alloca(bin.context.i8_type(), "scratch").unwrap(); + let vector_new = bin.module.get_function("vector_new").unwrap(); + let db_find = bin.module.get_function("db_find_i64").unwrap(); + let db_get = bin.module.get_function("db_get_i64").unwrap(); + let malloc = bin.module.get_function("__malloc").unwrap(); + + let found_bb = bin.context.append_basic_block(function, "str_found"); + let notfound_bb = bin.context.append_basic_block(function, "str_notfound"); + let merge_bb = bin.context.append_basic_block(function, "str_merge"); + + bin.builder.build_conditional_branch(found, found_bb, notfound_bb).unwrap(); + + // FOUND: read row, extract string bytes, create vector. + bin.builder.position_at_end(found_bb); + let pk = bin.builder.build_load(i64_ty, pk_out, "pk").unwrap().into_int_value(); + + let pri_iter = bin + .builder + .build_call(db_find, &[receiver.into(), receiver.into(), table_name.into(), pk.into()], "pri_iter") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + let row_total_size = bin + .builder + .build_call(db_get, &[pri_iter.into(), scratch_buf.into(), i32_ty.const_zero().into()], "row_size") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // Allocate row buffer via malloc (dynamic size). + let row_buf = bin + .builder + .build_call(malloc, &[row_total_size.into()], "row_buf") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_pointer_value(); + + // Need to re-find since db_get_i64 may have invalidated the iterator. + let pri_iter2 = bin + .builder + .build_call(db_find, &[receiver.into(), receiver.into(), table_name.into(), pk.into()], "pri_iter2") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + bin.builder + .build_call(db_get, &[pri_iter2.into(), row_buf.into(), row_total_size.into()], "") + .unwrap(); + + // Decode varuint32 at offset 40 to get string length and varuint byte count. + let fixed_header = i32_ty.const_int(40, false); // pk(8) + hash(32) + let varuint_ptr = unsafe { + bin.builder + .build_gep(bin.context.i8_type(), row_buf, &[fixed_header], "vi_ptr") + .unwrap() + }; + let decode_fn = bin.module.get_function("__decode_varuint32").unwrap(); + let packed = bin + .builder + .build_call(decode_fn, &[varuint_ptr.into()], "packed") + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + // Unpack: low 32 bits = string length, high 32 bits = varuint byte count + let str_len = bin + .builder + .build_int_truncate(packed, i32_ty, "str_len") + .unwrap(); + let varuint_size = bin + .builder + .build_int_truncate( + bin.builder + .build_right_shift(packed, bin.context.i64_type().const_int(32, false), false, "hi") + .unwrap(), + i32_ty, + "vi_sz", + ) + .unwrap(); + + // String data starts at offset 40 + varuint_size. + let data_offset = bin.builder.build_int_add(fixed_header, varuint_size, "doff").unwrap(); + let str_ptr = unsafe { + bin.builder + .build_gep(bin.context.i8_type(), row_buf, &[data_offset], "str_ptr") + .unwrap() + }; + + // Create a vector: vector_new(len, 1, data_ptr). + let found_vec = bin + .builder + .build_call( + vector_new, + &[str_len.into(), i32_ty.const_int(1, false).into(), str_ptr.into()], + "str_vec", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_pointer_value(); + bin.builder.build_unconditional_branch(merge_bb).unwrap(); + + // NOT FOUND: return empty vector. + bin.builder.position_at_end(notfound_bb); + let empty_vec = bin + .builder + .build_call( + vector_new, + &[i32_ty.const_zero().into(), i32_ty.const_int(1, false).into(), scratch_buf.into()], + "empty_vec", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_pointer_value(); + bin.builder.build_unconditional_branch(merge_bb).unwrap(); + + // MERGE. + bin.builder.position_at_end(merge_bb); + let ptr_ty = bin.context.ptr_type(inkwell::AddressSpace::default()); + let phi = bin.builder.build_phi(ptr_ty, "str_result").unwrap(); + phi.add_incoming(&[(&found_vec, found_bb), (&empty_vec, notfound_bb)]); + phi.as_basic_value().into_pointer_value() + } +} diff --git a/src/emit/binary.rs b/src/emit/binary.rs index 071aa53d4..b1eb0b92b 100644 --- a/src/emit/binary.rs +++ b/src/emit/binary.rs @@ -17,7 +17,7 @@ use tempfile::tempdir; use wasm_opt::OptimizationOptions; use crate::codegen::{cfg::ReturnCode, Options}; -use crate::emit::{polkadot, TargetRuntime}; +use crate::emit::{antelope, polkadot, TargetRuntime}; use crate::emit::{solana, BinaryOp, Generate}; use crate::linker::link; use crate::Target; @@ -191,6 +191,9 @@ impl<'a> Binary<'a> { Target::Soroban => { soroban::SorobanTarget::build(context, &std_lib, contract, ns, opt, _contract_no) } + Target::Antelope => { + antelope::AntelopeTarget::build(context, &std_lib, contract, ns, opt) + } _ => unimplemented!("target not implemented"), } } diff --git a/src/emit/expression.rs b/src/emit/expression.rs index ecd450e4f..4e55e4a92 100644 --- a/src/emit/expression.rs +++ b/src/emit/expression.rs @@ -1666,6 +1666,43 @@ pub(super) fn expression<'a, T: TargetRuntime<'a> + ?Sized>( values.push((v, len, e.ty())); } + // Antelope: every keccak256 is an internal storage-slot derivation of the + // shape keccak(prev_256 ‖ key). For the common 2-operand fixed-size-key case, + // call the shared __map_slot helper instead of inlining the preimage build at + // every storage access — this keeps storage-heavy functions small. The + // preimage bytes are identical, so the resulting slot hash is unchanged. + let antelope_slot = bin.ns.target == Target::Antelope + && values.len() == 2 + && values[0].0.is_int_value() + && values[0].0.into_int_value().get_type().get_bit_width() == 256 + && values[1].0.is_int_value() + && values[1].0.into_int_value().get_type().get_bit_width() <= 256 + && !matches!(values[1].2, Type::DynamicBytes | Type::String); + + if antelope_slot { + let i256_ty = bin.context.custom_width_int_type(256); + let prev = values[0].0.into_int_value(); + let key = values[1].0.into_int_value(); + let key = if key.get_type().get_bit_width() == 256 { + key + } else { + bin.builder + .build_int_z_extend(key, i256_ty, "key256") + .unwrap() + }; + return bin + .builder + .build_call( + bin.module.get_function("__map_slot").unwrap(), + &[prev.into(), key.into(), values[1].1.into()], + "map_slot", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap(); + } + // now allocate a buffer let src = bin .builder diff --git a/src/emit/mod.rs b/src/emit/mod.rs index 04e9cc266..ea13426c7 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -26,6 +26,7 @@ pub mod solana; #[cfg(feature = "soroban")] pub mod soroban; +pub mod antelope; mod storage; mod strings; diff --git a/src/lib.rs b/src/lib.rs index c6205ba4c..110f2d831 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,8 @@ pub enum Target { /// Ethereum EVM, see EVM, Soroban, + /// Antelope (EOS, WAX, Telos, etc.), see + Antelope, } impl fmt::Display for Target { @@ -42,6 +44,7 @@ impl fmt::Display for Target { Target::Polkadot { .. } => write!(f, "Polkadot"), Target::EVM => write!(f, "EVM"), Target::Soroban => write!(f, "Soroban"), + Target::Antelope => write!(f, "Antelope"), } } } @@ -55,6 +58,7 @@ impl PartialEq for Target { Target::Polkadot { .. } => matches!(other, Target::Polkadot { .. }), Target::EVM => matches!(other, Target::EVM), Target::Soroban => matches!(other, Target::Soroban), + Target::Antelope => matches!(other, Target::Antelope), } } } @@ -79,6 +83,7 @@ impl Target { "solana" => Some(Target::Solana), "polkadot" => Some(Target::default_polkadot()), "evm" => Some(Target::EVM), + "antelope" => Some(Target::Antelope), _ => None, } } diff --git a/src/linker/antelope_wasm.rs b/src/linker/antelope_wasm.rs new file mode 100644 index 000000000..c703dc399 --- /dev/null +++ b/src/linker/antelope_wasm.rs @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::ffi::CString; +use std::fs::File; +use std::io::Read; +use std::io::Write; +use tempfile::tempdir; +use wasm_encoder::{ + ConstExpr, EntityType, GlobalSection, GlobalType, ImportSection, MemoryType, Module, + RawSection, ValType, +}; +use wasmparser::{Global, Import, Parser, Payload::*, SectionLimited, TypeRef}; + +/// Link an Antelope WASM object file into a final .wasm binary. +/// +/// Antelope WASM requirements: +/// - Must export `apply(uint64, uint64, uint64)` entry point +/// - All host function imports come from the "env" module +/// - Memory is imported from "env" "memory" (not exported) +pub fn link(input: &[u8], name: &str) -> Vec { + let dir = tempdir().expect("failed to create temp directory for linking"); + + let object_filename = dir.path().join(format!("{name}.o")); + let res_filename = dir.path().join(format!("{name}.wasm")); + + let mut objectfile = + File::create(object_filename.clone()).expect("failed to create object file"); + + objectfile + .write_all(input) + .expect("failed to write object file to temp file"); + + let mut command_line = vec![ + CString::new("--no-entry").unwrap(), + CString::new("--allow-undefined").unwrap(), + CString::new("--gc-sections").unwrap(), + CString::new("--global-base=0").unwrap(), + CString::new("--initial-memory=1048576").unwrap(), // 1 MiB — heap at 0x10000, stack at top + CString::new("--export-dynamic").unwrap(), + ]; + + command_line.push( + CString::new( + object_filename + .to_str() + .expect("temp path should be unicode"), + ) + .unwrap(), + ); + command_line.push(CString::new("-o").unwrap()); + command_line + .push(CString::new(res_filename.to_str().expect("temp path should be unicode")).unwrap()); + + assert!(!super::wasm_linker(&command_line), "linker failed"); + + let mut output = Vec::new(); + let mut outputfile = File::open(res_filename).expect("output file should exist"); + outputfile + .read_to_end(&mut output) + .expect("failed to read output file"); + + // Post-process: remap imports to "env" module, set stack pointer global + generate_module(&output) +} + +/// Post-process the linked WASM: remap imports to "env" module and set stack pointer. +fn generate_module(input: &[u8]) -> Vec { + let mut module = Module::new(); + for payload in Parser::new(0).parse_all(input).map(|s| s.unwrap()) { + match payload { + ImportSection(s) => generate_import_section(s, &mut module), + GlobalSection(s) => generate_global_section(s, &mut module), + ModuleSection { .. } | ComponentSection { .. } => panic!("nested WASM module"), + _ => { + if let Some((id, range)) = payload.as_section() { + module.section(&RawSection { + id, + data: &input[range], + }); + } + } + } + } + module.finish() +} + +/// Rewrite all imports to use "env" module. +fn generate_import_section(section: SectionLimited, module: &mut Module) { + let mut imports = ImportSection::new(); + for import in section.into_iter().map(|import| import.unwrap()) { + let import_type = match import.ty { + TypeRef::Func(n) => EntityType::Function(n), + TypeRef::Memory(m) => EntityType::Memory(MemoryType { + maximum: m.maximum, + minimum: m.initial, + memory64: m.memory64, + shared: m.shared, + }), + _ => panic!("unexpected WASM import type {import:?}"), + }; + + // All Antelope host functions live in the "env" module. + imports.import("env", import.name, import_type); + } + module.section(&imports); +} + +/// Set the stack pointer global to top of memory (1 MiB). +/// Stack grows downward from 1 MiB; heap grows upward from 0x10000. +fn generate_global_section(_section: SectionLimited, module: &mut Module) { + let mut globals = GlobalSection::new(); + let global_type = GlobalType { + val_type: ValType::I32, + mutable: true, + }; + // Stack pointer at top of 1 MiB initial memory + globals.global(global_type, &ConstExpr::i32_const(1048576)); + module.section(&globals); +} diff --git a/src/linker/mod.rs b/src/linker/mod.rs index 30ecb10d7..ed1316889 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +mod antelope_wasm; mod bpf; mod polkadot_wasm; mod soroban_wasm; @@ -23,6 +24,7 @@ pub fn link(input: &[u8], name: &str, target: Target) -> Vec { address_length: _, value_length: _, } => polkadot_wasm::link(input, name), + Target::Antelope => antelope_wasm::link(input, name), _ => panic!("linker not implemented for target {target:?}"), } } diff --git a/src/sema/ast.rs b/src/sema/ast.rs index 0d09dfc9f..61cf43919 100644 --- a/src/sema/ast.rs +++ b/src/sema/ast.rs @@ -1840,6 +1840,38 @@ pub enum Builtin { AuthAsCurrContract, ExtendTtl, ExtendInstanceTtl, + AntelopeRequireAuth, + AntelopeSelf, + AntelopeCode, + AntelopeName, + AntelopeRequireRecipient, + AntelopeCall, + AntelopeCallAuth, + AntelopeSetPayer, + AntelopePack, + AntelopeHasAuth, + AntelopeRequireAuth2, + AntelopeTimestamp, + // Decode helpers + AntelopeToUint64, + AntelopeToInt64, + AntelopeToUint32, + AntelopeToUint128, + AntelopeToBytes32, + AntelopeToString, + // Table read builtins + AntelopeDbFind, + AntelopeDbGet, + AntelopeDbNext, + AntelopeDbLowerbound, + AntelopeLastPk, + // Secondary index builtins + AntelopeDbIdx64Find, + AntelopeDbIdx64Lowerbound, + AntelopeDbIdx128Find, + AntelopeDbIdx128Lowerbound, + AntelopeDbIdx256Find, + AntelopeDbIdx256Lowerbound, } #[derive(PartialEq, Eq, Clone, Debug)] diff --git a/src/sema/builtin.rs b/src/sema/builtin.rs index 91d68758a..5fce6bc70 100644 --- a/src/sema/builtin.rs +++ b/src/sema/builtin.rs @@ -36,7 +36,7 @@ pub struct Prototype { } // A list of all Solidity builtins functions -pub static BUILTIN_FUNCTIONS: Lazy<[Prototype; 29]> = Lazy::new(|| { +pub static BUILTIN_FUNCTIONS: Lazy<[Prototype; 58]> = Lazy::new(|| { // 29 original + 12 Antelope + 17 table/decode [ Prototype { builtin: Builtin::ExtendInstanceTtl, @@ -369,6 +369,328 @@ pub static BUILTIN_FUNCTIONS: Lazy<[Prototype; 29]> = Lazy::new(|| { doc: "Authorizes sub-contract calls for the next contract call on behalf of the current contract.", constant: false, }, + Prototype { + builtin: Builtin::AntelopeRequireAuth, + namespace: Some("antelope"), + method: vec![], + name: "requireAuth", + params: vec![Type::Uint(64)], + ret: vec![], + target: vec![Target::Antelope], + doc: "Require authorization from the given Antelope account. Aborts if not authorized.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeSelf, + namespace: Some("antelope"), + method: vec![], + name: "self", + params: vec![], + ret: vec![Type::Uint(64)], + target: vec![Target::Antelope], + doc: "Returns the current contract's Antelope account name as uint64.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeCode, + namespace: Some("antelope"), + method: vec![], + name: "code", + params: vec![], + ret: vec![Type::Uint(64)], + target: vec![Target::Antelope], + doc: "Returns the code account of the current action. Equals receiver() on direct calls; differs on notifications (require_recipient).", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeName, + namespace: Some("antelope"), + method: vec![], + name: "name", + params: vec![Type::String], + ret: vec![Type::Uint(64)], + target: vec![Target::Antelope], + doc: "Encode a string literal as an Antelope eosio::name uint64 at compile time.", + constant: true, + }, + Prototype { + builtin: Builtin::AntelopeRequireRecipient, + namespace: Some("antelope"), + method: vec![], + name: "requireRecipient", + params: vec![Type::Uint(64)], + ret: vec![], + target: vec![Target::Antelope], + doc: "Send a notification to the given account. The account's contract will receive the current action.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeCall, + namespace: Some("antelope"), + method: vec![], + name: "call", + params: vec![Type::Uint(64), Type::Uint(64), Type::DynamicBytes], + ret: vec![], + target: vec![Target::Antelope], + doc: "Send an inline action to another contract with self@active auth. Args: contract (uint64), action_name (uint64), packed_data (bytes).", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeCallAuth, + namespace: Some("antelope"), + method: vec![], + name: "callauth", + params: vec![Type::Uint(64), Type::Uint(64), Type::DynamicBytes, Type::Uint(64), Type::Uint(64)], + ret: vec![], + target: vec![Target::Antelope], + doc: "Send an inline action with explicit auth. Args: contract, action_name, data, actor, permission.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeSetPayer, + namespace: Some("antelope"), + method: vec![], + name: "setpayer", + params: vec![Type::Uint(64)], + ret: vec![], + target: vec![Target::Antelope], + doc: "Set the RAM payer for subsequent storage operations. Defaults to self if not called.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopePack, + namespace: Some("antelope"), + method: vec![], + name: "pack", + params: vec![], + ret: vec![Type::DynamicBytes], + target: vec![Target::Antelope], + doc: "Pack arguments into Antelope CDT format (little-endian integers, varuint32-prefixed strings). Variadic: accepts any number of typed arguments.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeHasAuth, + namespace: Some("antelope"), + method: vec![], + name: "hasAuth", + params: vec![Type::Uint(64)], + ret: vec![Type::Bool], + target: vec![Target::Antelope], + doc: "Returns true if the transaction includes authorization for the given Antelope account. Does not abort on failure.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeRequireAuth2, + namespace: Some("antelope"), + method: vec![], + name: "requireAuth2", + params: vec![Type::Uint(64), Type::Uint(64)], + ret: vec![], + target: vec![Target::Antelope], + doc: "Require explicit permission-level authorization. Args: account (uint64), permission (uint64, e.g. antelope.name(\"active\")).", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeTimestamp, + namespace: Some("antelope"), + method: vec![], + name: "timestamp", + params: vec![], + ret: vec![Type::Uint(64)], + target: vec![Target::Antelope], + doc: "Returns the current block time as microseconds since Unix epoch (current_time() host function).", + constant: false, + }, + // --- Decode helpers --- + Prototype { + builtin: Builtin::AntelopeToUint64, + namespace: Some("antelope"), + method: vec![], + name: "toUint64", + params: vec![Type::DynamicBytes, Type::Uint(32)], + ret: vec![Type::Uint(64)], + target: vec![Target::Antelope], + doc: "Read 8 bytes little-endian from bytes buffer at offset, returns uint64.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeToInt64, + namespace: Some("antelope"), + method: vec![], + name: "toInt64", + params: vec![Type::DynamicBytes, Type::Uint(32)], + ret: vec![Type::Int(64)], + target: vec![Target::Antelope], + doc: "Read 8 bytes little-endian from bytes buffer at offset, returns int64.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeToUint32, + namespace: Some("antelope"), + method: vec![], + name: "toUint32", + params: vec![Type::DynamicBytes, Type::Uint(32)], + ret: vec![Type::Uint(32)], + target: vec![Target::Antelope], + doc: "Read 4 bytes little-endian from bytes buffer at offset, returns uint32.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeToUint128, + namespace: Some("antelope"), + method: vec![], + name: "toUint128", + params: vec![Type::DynamicBytes, Type::Uint(32)], + ret: vec![Type::Uint(128)], + target: vec![Target::Antelope], + doc: "Read 16 bytes little-endian from bytes buffer at offset, returns uint128.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeToBytes32, + namespace: Some("antelope"), + method: vec![], + name: "toBytes32", + params: vec![Type::DynamicBytes, Type::Uint(32)], + ret: vec![Type::Bytes(32)], + target: vec![Target::Antelope], + doc: "Read 32 bytes from bytes buffer at offset, returns bytes32.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeToString, + namespace: Some("antelope"), + method: vec![], + name: "toString", + params: vec![Type::DynamicBytes, Type::Uint(32)], + ret: vec![Type::String], + target: vec![Target::Antelope], + doc: "Read varuint32-prefixed string from bytes buffer at offset.", + constant: false, + }, + // --- Table read builtins --- + Prototype { + builtin: Builtin::AntelopeDbFind, + namespace: Some("antelope"), + method: vec![], + name: "dbFind", + params: vec![Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64)], + ret: vec![Type::Int(32)], + target: vec![Target::Antelope], + doc: "Find a row by primary key. Args: code, scope, table, pk. Returns iterator (<0 if not found).", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeDbGet, + namespace: Some("antelope"), + method: vec![], + name: "dbGet", + params: vec![Type::Int(32)], + ret: vec![Type::DynamicBytes], + target: vec![Target::Antelope], + doc: "Read row data from an iterator. Returns raw bytes.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeDbNext, + namespace: Some("antelope"), + method: vec![], + name: "dbNext", + params: vec![Type::Int(32)], + ret: vec![Type::Int(32)], + target: vec![Target::Antelope], + doc: "Advance iterator to next row. Returns new iterator (<0 if end). Primary key cached in lastPk().", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeDbLowerbound, + namespace: Some("antelope"), + method: vec![], + name: "dbLowerbound", + params: vec![Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64)], + ret: vec![Type::Int(32)], + target: vec![Target::Antelope], + doc: "Find first row with pk >= id. Args: code, scope, table, id. Returns iterator.", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeLastPk, + namespace: Some("antelope"), + method: vec![], + name: "lastPk", + params: vec![], + ret: vec![Type::Uint(64)], + target: vec![Target::Antelope], + doc: "Returns the primary key cached by the last dbNext/dbIdx*Find/dbIdx*Lowerbound call.", + constant: false, + }, + // --- Secondary index builtins --- + Prototype { + builtin: Builtin::AntelopeDbIdx64Find, + namespace: Some("antelope"), + method: vec![], + name: "dbIdx64Find", + params: vec![Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64)], + ret: vec![Type::Int(32)], + target: vec![Target::Antelope], + doc: "Find by idx64 secondary key. Args: code, scope, table, indexNum, key. Returns iterator. PK in lastPk().", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeDbIdx64Lowerbound, + namespace: Some("antelope"), + method: vec![], + name: "dbIdx64Lowerbound", + params: vec![Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64)], + ret: vec![Type::Int(32)], + target: vec![Target::Antelope], + doc: "Find first idx64 entry >= key. Args: code, scope, table, indexNum, key. Returns iterator. PK in lastPk().", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeDbIdx128Find, + namespace: Some("antelope"), + method: vec![], + name: "dbIdx128Find", + params: vec![Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(128)], + ret: vec![Type::Int(32)], + target: vec![Target::Antelope], + doc: "Find by idx128 secondary key. Args: code, scope, table, indexNum, key. Returns iterator. PK in lastPk().", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeDbIdx128Lowerbound, + namespace: Some("antelope"), + method: vec![], + name: "dbIdx128Lowerbound", + params: vec![Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(128)], + ret: vec![Type::Int(32)], + target: vec![Target::Antelope], + doc: "Find first idx128 entry >= key. Args: code, scope, table, indexNum, key. Returns iterator. PK in lastPk().", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeDbIdx256Find, + namespace: Some("antelope"), + method: vec![], + name: "dbIdx256Find", + params: vec![Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Bytes(32)], + ret: vec![Type::Int(32)], + target: vec![Target::Antelope], + doc: "Find by idx256 secondary key. Args: code, scope, table, indexNum, key (bytes32). Returns iterator. PK in lastPk().", + constant: false, + }, + Prototype { + builtin: Builtin::AntelopeDbIdx256Lowerbound, + namespace: Some("antelope"), + method: vec![], + name: "dbIdx256Lowerbound", + params: vec![Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Uint(64), Type::Bytes(32)], + ret: vec![Type::Int(32)], + target: vec![Target::Antelope], + doc: "Find first idx256 entry >= key. Args: code, scope, table, indexNum, key (bytes32). Returns iterator. PK in lastPk().", + constant: false, + }, ] }); @@ -942,6 +1264,14 @@ pub fn builtin_var( ), )); } + if ns.target == Target::Antelope && p.builtin == Builtin::Value { + diagnostics.push(Diagnostic::error( + *loc, + String::from( + "Antelope does not support value transfers in calls. Use explicit token transfer actions instead.", + ), + )); + } if ns.target == Target::Solana && p.builtin == Builtin::Sender { diagnostics.push(Diagnostic::error( *loc, @@ -1149,6 +1479,36 @@ pub(super) fn resolve_namespace_call( }); } + // antelope.pack(...) is variadic — resolve all args with unknown type + if namespace == "antelope" && name == "pack" { + let mut resolved_args = Vec::new(); + for arg in args { + let mut expr = expression(arg, context, ns, symtable, diagnostics, ResolveTo::Unknown)?; + // Byte/string literals come through as BytesLiteral{ty:Bytes(0)} — cast to String + // so the emit layer sees Type::String, not a 0-bit integer. + if let Expression::BytesLiteral { .. } = &expr { + expr = expr.cast(&arg.loc(), &Type::String, true, ns, diagnostics)?; + } + // StorageRef args (state variables) need an explicit load, since + // ResolveTo::Unknown doesn't trigger the implicit dereference. + if let Type::StorageRef(_, inner_ty) = expr.ty() { + expr = Expression::StorageLoad { + loc: arg.loc(), + ty: *inner_ty, + expr: Box::new(expr), + }; + } + resolved_args.push(expr); + + } + return Ok(Expression::Builtin { + loc: *loc, + tys: vec![Type::DynamicBytes], + kind: Builtin::AntelopePack, + args: resolved_args, + }); + } + // The abi.* functions need special handling, others do not if namespace != "abi" && namespace != "string" { return resolve_call( diff --git a/src/sema/contracts.rs b/src/sema/contracts.rs index 910b28b37..576227900 100644 --- a/src/sema/contracts.rs +++ b/src/sema/contracts.rs @@ -92,6 +92,7 @@ pub fn resolve(contracts: &[ContractDefinition], file_no: usize, ns: &mut ast::N check_inheritance(contract_no, ns); mangle_function_names(contract_no, ns); verify_unique_selector(contract_no, ns); + antelope_verify_unique_action_names(contract_no, ns); polkadot_requires_public_functions(contract_no, ns); unique_constructor_names(contract_no, ns); check_mangled_function_names(contract_no, ns); @@ -1338,3 +1339,57 @@ fn verify_unique_selector(contract_no: usize, ns: &mut Namespace) { ns.diagnostics.append(&mut diagnostics); } + +/// Antelope: every public/external function becomes an action whose name is the +/// eosio::name normalization of the Solidity function name (see +/// `abi::antelope::normalize_action_name`). Two *distinct* functions can normalize +/// to the same action name — via case-folding (`putHash` / `puthash`), overloading +/// (`foo(uint64)` / `foo(string)`, both → `foo`), or charset stripping/truncation. +/// The emitter's dispatch switch would then route only the first match, silently +/// leaving the others unreachable. The per-function charset diagnostic (in +/// `sema::functions`) rejects names that strip; this contract-level pass catches +/// the collisions those miss — case-folding and overloads, where each individual +/// name normalizes cleanly yet they land on the same action. +fn antelope_verify_unique_action_names(contract_no: usize, ns: &mut Namespace) { + if ns.target != crate::Target::Antelope { + return; + } + + let mut seen: HashMap = HashMap::new(); + let mut diagnostics: Vec = Vec::new(); + + // Mirror the exact set of functions that `abi::antelope::gen_abi` turns into + // actions: public/external, non-constructor. + for func_no in ns.contracts[contract_no].all_functions.keys() { + let func = &ns.functions[*func_no]; + + if !func.is_public() || func.ty == FunctionTy::Constructor { + continue; + } + + let action = crate::abi::antelope::normalize_action_name(&func.id.name); + // Empty (whole name stripped) is already reported per-function; skip here. + if action.is_empty() { + continue; + } + + if let Some(other_no) = seen.get(&action) { + let other = &ns.functions[*other_no]; + diagnostics.push(ast::Diagnostic::error_with_note( + func.loc_prototype, + format!( + "function '{}' collides with '{}': both map to the Antelope action name '{}'. \ + Action names must be unique — rename one. (Function overloading is not \ + supported for Antelope actions, since an action carries no parameter signature.)", + func.id.name, other.id.name, action + ), + other.loc_prototype, + format!("'{}' also maps to action '{}'", other.id.name, action), + )); + } else { + seen.insert(action, *func_no); + } + } + + ns.diagnostics.append(&mut diagnostics); +} diff --git a/src/sema/expression/constructor.rs b/src/sema/expression/constructor.rs index d69d33808..63d8f18d4 100644 --- a/src/sema/expression/constructor.rs +++ b/src/sema/expression/constructor.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::sema::ast::{ArrayLength, CallArgs, Expression, Namespace, Note, RetrieveType, Type}; +use crate::Target; use crate::sema::diagnostics::Diagnostics; use crate::sema::expression::function_call::{ collect_call_args, evaluate_argument, parse_call_args, @@ -27,6 +28,15 @@ fn constructor( symtable: &mut Symtable, diagnostics: &mut Diagnostics, ) -> Result { + if ns.target == Target::Antelope { + diagnostics.push(Diagnostic::error( + *loc, + "dynamic contract creation with 'new' is not supported on Antelope. Deploy contracts separately." + .to_string(), + )); + return Err(()); + } + if !ns.contracts[no].instantiable { diagnostics.push(Diagnostic::error( *loc, diff --git a/src/sema/expression/function_call.rs b/src/sema/expression/function_call.rs index 756c0d8e6..aa9412e0d 100644 --- a/src/sema/expression/function_call.rs +++ b/src/sema/expression/function_call.rs @@ -1371,7 +1371,7 @@ fn try_type_method( let ty = match func.name.as_str() { "call" => Some(CallTy::Regular), - "delegatecall" if ns.target != Target::Solana => Some(CallTy::Delegate), + "delegatecall" if ns.target != Target::Solana && ns.target != Target::Antelope => Some(CallTy::Delegate), "staticcall" if ns.target == Target::EVM => Some(CallTy::Static), _ => None, }; diff --git a/src/sema/functions.rs b/src/sema/functions.rs index 720d2bddc..8792dccb5 100644 --- a/src/sema/functions.rs +++ b/src/sema/functions.rs @@ -19,6 +19,30 @@ use solang_parser::{ pt::{CodeLocation, OptionalCodeLocation}, }; +/// Whether a parameter type can be deserialized from Antelope action data. +/// +/// Mirrors exactly the type set the emitter's action-data deserialization handles +/// (`datastream_fixed_size` in `emit/antelope/mod.rs` returns `Some` for the +/// fixed-size arms and `None` for `string`/`bytes`, which the emitter reads as a +/// varuint32 length + bytes). Every other type panics the emitter, so we keep this +/// predicate deliberately narrow — anything it does not list is rejected with a +/// clean diagnostic in `contract_function` rather than aborting codegen. +fn antelope_action_param_supported(ty: &Type) -> bool { + matches!( + ty, + Type::Bool + | Type::Uint(_) + | Type::Int(_) + | Type::Bytes(_) + | Type::Enum(_) + | Type::Value + | Type::Contract(_) + | Type::Address(_) + | Type::String + | Type::DynamicBytes + ) +} + /// Resolve function declaration in a contract pub fn contract_function( contract: &ContractDefinition, @@ -480,9 +504,84 @@ pub fn contract_function( fdecl.is_override = is_override; fdecl.has_body = func.body.is_some(); + // Antelope does not support payable functions. + if ns.target == Target::Antelope && fdecl.is_payable() { + ns.diagnostics.push(Diagnostic::error( + func.loc_prototype, + "Antelope does not support payable functions. Use explicit token transfer actions instead." + .to_string(), + )); + return None; + } + + // Antelope action names derive from public/external function names via the + // eosio::name charset (lowercase a-z, digits 1-5, dot; max 12 chars). Any + // character outside that set — or a name longer than 12 chars — is silently + // dropped/truncated when the action name is encoded, which mangles the + // on-chain name and can collide two functions onto the same action (the + // second dispatch branch then becomes unreachable). Reject it at compile time + // rather than letting it mis-dispatch or fail at `cleos set abi`. Case-only + // changes (idiomatic camelCase like `putHash`) are fine — those normalize + // predictably to lowercase, so `action == lowered` and no error fires. + if ns.target == Target::Antelope + && fdecl.is_public() + && func.ty == pt::FunctionTy::Function + { + let lowered: String = fdecl.id.name.chars().flat_map(char::to_lowercase).collect(); + let action = crate::abi::antelope::normalize_action_name(&fdecl.id.name); + if action != lowered { + ns.diagnostics.push(Diagnostic::error( + func.loc_prototype, + format!( + "public function '{}' cannot be represented as an Antelope action name: \ + the eosio::name charset only allows lowercase a-z, digits 1-5 and dot, with \ + at most 12 characters. It would be truncated/mangled to '{}' (risking a \ + collision with another action). Rename it to fit the charset — camelCase is \ + fine, it lowercases to a predictable name.", + fdecl.id.name, action + ), + )); + } + + // Antelope action-data deserialization only handles fixed-size scalars + // (bool, intN/uintN, bytesN, enum, address) plus variable-length + // `string`/`bytes`. Compound parameters (dynamic arrays, structs, ...) + // are not implemented and previously aborted the emitter with an internal + // panic ("unsupported parameter type for action data deserialization"). + // Reject them here with a clean diagnostic instead. Pass such data as + // `bytes` and decode it inside the action for now. + for param in fdecl.params.iter() { + if !antelope_action_param_supported(¶m.ty) { + let loc = param.loc; + ns.diagnostics.push(Diagnostic::error( + loc, + format!( + "parameter type '{}' is not supported for Antelope action '{}'. \ + Actions accept fixed-size scalars (bool, intN/uintN, bytesN, enum, \ + address), 'string' and 'bytes'; arrays, structs and other compound \ + types are not yet deserializable — pass the data as 'bytes' and decode \ + it in the action body.", + param.ty.to_string(ns), + action + ), + )); + } + } + } + function_prototype_annotations(&mut fdecl, annotations, ns); if func.ty == pt::FunctionTy::Constructor { + // Antelope has no deploy-time initialization. + if ns.target == Target::Antelope { + ns.diagnostics.push(Diagnostic::error( + func.loc_prototype, + "constructors are not supported on Antelope. Use an explicit init() action instead." + .to_string(), + )); + return None; + } + // In the eth solidity only one constructor is allowed if ns.target == Target::EVM { if let Some(prev_func_no) = ns.contracts[contract_no] @@ -640,6 +739,10 @@ pub fn contract_function( let func_no = ns.functions.len(); ns.functions.push(fdecl); + + // Note: Antelope return values are passed via set_action_return_value (Leap 3.x+). + // Variable-length return types (string, bytes) are not yet serialized. + ns.contracts[contract_no].functions.push(func_no); if let Some(Symbol::Function(ref mut v)) = diff --git a/src/sema/namespace.rs b/src/sema/namespace.rs index 8915935b9..774138b93 100644 --- a/src/sema/namespace.rs +++ b/src/sema/namespace.rs @@ -42,6 +42,7 @@ impl Namespace { } => (address_length, value_length), Target::Solana => (32, 8), Target::Soroban => (32, 64), + Target::Antelope => (20, 8), }; let mut ns = Namespace { diff --git a/src/sema/statements.rs b/src/sema/statements.rs index ffa089cfc..4e4a821b7 100644 --- a/src/sema/statements.rs +++ b/src/sema/statements.rs @@ -830,6 +830,14 @@ fn statement( flags, block, } => { + if ns.target == Target::Antelope { + ns.diagnostics.push(Diagnostic::error( + *loc, + "inline assembly is not supported on Antelope. Use antelope.* builtins instead." + .to_string(), + )); + return Err(()); + } if dialect.is_some() && dialect.as_ref().unwrap().string != "evmasm" { ns.diagnostics.push(Diagnostic::error( dialect.as_ref().unwrap().loc, @@ -1319,6 +1327,7 @@ fn emit_event( event.used = true; diagnostics.extend(candidate_diagnostics); + antelope_check_event_name(event_no, loc, ns, diagnostics); return Ok(stmt); } else { @@ -1498,6 +1507,8 @@ fn emit_event( let event = &mut ns.events[event_no]; event.used = true; + antelope_check_event_name(event_no, loc, ns, diagnostics); + return Ok(stmt); } else { diagnostics.push(Diagnostic::error_with_notes( @@ -1531,6 +1542,41 @@ fn emit_event( Err(()) } +/// For the Antelope target: warn if an event name will be silently truncated. +/// Antelope action names are max 12 chars (lowercase + digits + dot). +/// We prefix with "e." (2 chars), leaving 10 chars for the filtered event name. +fn antelope_check_event_name( + event_no: usize, + loc: &pt::Loc, + ns: &Namespace, + diagnostics: &mut Diagnostics, +) { + if ns.target != crate::Target::Antelope { + return; + } + let event_name = &ns.events[event_no].id.name; + let filtered_len: usize = event_name + .chars() + .filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) + .count(); + if filtered_len > 10 { + let truncated: String = event_name + .to_lowercase() + .chars() + .filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) + .take(10) + .collect(); + diagnostics.push(Diagnostic::warning( + *loc, + format!( + "event name '{}' has {} valid characters after filtering but only 10 fit; \ + action name will be 'e.{}'", + event_name, filtered_len, truncated + ), + )); + } +} + fn remove_duplicate_events( loc: &pt::Loc, resolved_events: &mut Vec<(usize, Diagnostics, Statement)>, diff --git a/src/sema/yul/builtin.rs b/src/sema/yul/builtin.rs index 48a913830..8ae0932ed 100644 --- a/src/sema/yul/builtin.rs +++ b/src/sema/yul/builtin.rs @@ -22,7 +22,7 @@ impl YulBuiltinPrototype { Target::EVM => self.availability[0], Target::Polkadot { .. } => self.availability[1], Target::Solana => self.availability[2], - Target::Soroban => unimplemented!(), + Target::Soroban | Target::Antelope => unimplemented!(), } } } diff --git a/tests/antelope.rs b/tests/antelope.rs new file mode 100644 index 000000000..47729ba60 --- /dev/null +++ b/tests/antelope.rs @@ -0,0 +1,703 @@ +// SPDX-License-Identifier: Apache-2.0 + +/// Mock runtime for the Antelope target. +/// Executes compiled Antelope WASM in wasmi with mocked host functions. + +use std::collections::HashMap; +use std::ffi::OsStr; +use tiny_keccak::{Hasher, Keccak}; +use wasmi::core::{Trap, TrapCode}; +use wasmi::{Engine, Error, Linker, Memory, Module, Store, Value}; + +use solang::codegen::Options; +use solang::file_resolver::FileResolver; +use solang::{compile, Target}; + +use wasm_host_attr::wasm_host; + +mod antelope_tests; + +// ─── Antelope name encoding ─── + +/// Encode a string as an Antelope eosio::name uint64. +pub fn string_to_name(s: &str) -> u64 { + let char_to_value = |c: u8| -> u64 { + match c { + b'.' => 0, + b'1'..=b'5' => (c - b'1' + 1) as u64, + b'a'..=b'z' => (c - b'a' + 6) as u64, + _ => 0, + } + }; + let bytes = s.as_bytes(); + let len = bytes.len().min(13); + let mut value: u64 = 0; + for i in 0..len.min(12) { + value |= char_to_value(bytes[i]) << (64 - 5 * (i + 1)); + } + if len == 13 { + value |= char_to_value(bytes[12]) & 0x0F; + } + value +} + +// ─── DataStream encoding helpers ─── + +pub enum ActionParam { + U64(u64), + I64(i64), + U32(u32), + Bool(bool), + String(String), +} + +pub fn encode_action_data(params: &[ActionParam]) -> Vec { + let mut buf = Vec::new(); + for param in params { + match param { + ActionParam::U64(v) => buf.extend_from_slice(&v.to_le_bytes()), + ActionParam::I64(v) => buf.extend_from_slice(&v.to_le_bytes()), + ActionParam::U32(v) => buf.extend_from_slice(&v.to_le_bytes()), + ActionParam::Bool(v) => buf.push(if *v { 1 } else { 0 }), + ActionParam::String(s) => { + encode_varuint32(&mut buf, s.len() as u32); + buf.extend_from_slice(s.as_bytes()); + } + } + } + buf +} + +fn encode_varuint32(buf: &mut Vec, mut val: u32) { + loop { + let mut byte = (val & 0x7F) as u8; + val >>= 7; + if val != 0 { + byte |= 0x80; + } + buf.push(byte); + if val == 0 { + break; + } + } +} + +// ─── Table storage types ─── + +type TableKey = (u64, u64, u64); // (code, scope, table) + +#[derive(Clone, Debug)] +pub struct TableRow { + pub pk: u64, + pub data: Vec, +} + +#[derive(Clone, Debug)] +pub struct SecondaryIdx256 { + pub pk: u64, + pub value: [u8; 32], + pub iter_id: i32, +} + +#[derive(Clone, Debug, Default)] +pub struct Table { + pub rows: Vec, + pub idx256: Vec, +} + +// ─── Runtime state ─── + +pub struct Runtime { + pub memory: Option, + pub receiver: u64, + pub action_data: Vec, + pub auth_accounts: Vec, + pub prints: String, + pub tables: HashMap, + pub inline_actions: Vec>, + pub notifications: Vec, + // Iterator tracking + next_iter: i32, + next_end_iter: i32, + iter_map: HashMap, // normal iter -> (table, row_idx) + end_iter_map: HashMap, // end iter -> table key + // Secondary index iterators + idx256_iter_map: HashMap, // sec iter -> (table, idx_pos) +} + +impl Runtime { + fn new(receiver: u64) -> Self { + Self { + memory: None, + receiver, + action_data: Vec::new(), + auth_accounts: vec![receiver], + prints: String::new(), + tables: HashMap::new(), + inline_actions: Vec::new(), + notifications: Vec::new(), + next_iter: 0, + next_end_iter: -2, + iter_map: HashMap::new(), + end_iter_map: HashMap::new(), + idx256_iter_map: HashMap::new(), + } + } + + fn alloc_iter(&mut self, key: TableKey, row_idx: usize) -> i32 { + let id = self.next_iter; + self.next_iter += 1; + self.iter_map.insert(id, (key, row_idx)); + id + } + + fn alloc_end_iter(&mut self, key: TableKey) -> i32 { + let id = self.next_end_iter; + self.next_end_iter -= 1; + self.end_iter_map.insert(id, key); + id + } + + fn alloc_idx256_iter(&mut self, key: TableKey, idx_pos: usize) -> i32 { + let id = self.next_iter; + self.next_iter += 1; + self.idx256_iter_map.insert(id, (key, idx_pos)); + id + } +} + +// ─── Memory helpers ─── + +fn read_buf(mem: &[u8], ptr: u32, len: u32) -> Vec { + mem[ptr as usize..(ptr + len) as usize].to_vec() +} + +fn write_buf(mem: &mut [u8], ptr: u32, data: &[u8]) { + mem[ptr as usize..ptr as usize + data.len()].copy_from_slice(data); +} + +fn read_string(mem: &[u8], ptr: u32) -> String { + let start = ptr as usize; + let end = mem[start..].iter().position(|&b| b == 0).unwrap_or(0) + start; + String::from_utf8_lossy(&mem[start..end]).to_string() +} + +// ─── Host function implementations ─── + +#[wasm_host] +impl Runtime { + // --- Print / Assert --- + + #[host("env")] + fn prints_l(ptr: u32, len: u32) -> Result<(), Trap> { + let s = String::from_utf8_lossy(&read_buf(mem, ptr, len)).to_string(); + vm.prints.push_str(&s); + Ok(()) + } + + #[host("env")] + fn eosio_assert(test: u32, msg_ptr: u32) -> Result<(), Trap> { + if test == 0 { + let msg = read_string(mem, msg_ptr); + eprintln!("eosio_assert failed: {msg}"); + return Err(TrapCode::UnreachableCodeReached.into()); + } + Ok(()) + } + + // --- Action data --- + + #[host("env")] + fn action_data_size() -> Result { + Ok(vm.action_data.len() as u32) + } + + #[host("env")] + fn read_action_data(ptr: u32, len: u32) -> Result { + let copy_len = std::cmp::min(len as usize, vm.action_data.len()); + write_buf(mem, ptr, &vm.action_data[..copy_len]); + Ok(copy_len as u32) + } + + // --- Auth --- + + #[host("env")] + fn require_auth(account: u64) -> Result<(), Trap> { + if !vm.auth_accounts.contains(&account) { + eprintln!("require_auth: missing authority for account {account}"); + return Err(TrapCode::UnreachableCodeReached.into()); + } + Ok(()) + } + + #[host("env")] + fn has_auth(account: u64) -> Result { + Ok(if vm.auth_accounts.contains(&account) { 1 } else { 0 }) + } + + #[host("env")] + fn require_auth2(account: u64, _permission: u64) -> Result<(), Trap> { + if !vm.auth_accounts.contains(&account) { + eprintln!("require_auth2: missing authority for account {account}"); + return Err(TrapCode::UnreachableCodeReached.into()); + } + Ok(()) + } + + // --- Identity --- + + #[host("env")] + fn current_receiver() -> Result { + Ok(vm.receiver) + } + + #[host("env")] + fn current_time() -> Result { + // Return a fixed mock timestamp: 2024-01-01T00:00:00Z in microseconds + Ok(1704067200_000_000u64) + } + + // --- Notifications --- + + #[host("env")] + fn require_recipient(name: u64) -> Result<(), Trap> { + vm.notifications.push(name); + Ok(()) + } + + // --- Inline actions --- + + #[host("env")] + fn send_inline(ptr: u32, len: u32) -> Result<(), Trap> { + let data = read_buf(mem, ptr, len); + vm.inline_actions.push(data); + Ok(()) + } + + // --- Return value --- + + #[host("env")] + fn set_action_return_value(_ptr: u32, _len: u32) -> Result<(), Trap> { + // In a real environment this captures the return value; mock ignores it. + Ok(()) + } + + // --- Crypto --- + + #[host("env")] + fn sha3(data_ptr: u32, data_len: u32, hash_ptr: u32, _hash_len: u32, _keccak: u32) -> Result<(), Trap> { + let data = read_buf(mem, data_ptr, data_len); + let mut hasher = Keccak::v256(); + hasher.update(&data); + let mut output = [0u8; 32]; + hasher.finalize(&mut output); + write_buf(mem, hash_ptr, &output); + Ok(()) + } + + // --- Primary table: db_store_i64 --- + + #[host("env")] + fn db_store_i64(scope: u64, table: u64, payer: u64, id: u64, data_ptr: u32, data_len: u32) -> Result { + let _ = payer; + let key: TableKey = (vm.receiver, scope, table); + let data = read_buf(mem, data_ptr, data_len); + let tbl = vm.tables.entry(key).or_default(); + tbl.rows.push(TableRow { pk: id, data }); + let row_idx = tbl.rows.len() - 1; + Ok(vm.alloc_iter(key, row_idx)) + } + + // --- Primary table: db_find_i64 --- + + #[host("env")] + fn db_find_i64(code: u64, scope: u64, table: u64, id: u64) -> Result { + let key: TableKey = (code, scope, table); + if let Some(tbl) = vm.tables.get(&key) { + if let Some(pos) = tbl.rows.iter().position(|r| r.pk == id) { + return Ok(vm.alloc_iter(key, pos)); + } + } + Ok(-1) + } + + // --- Primary table: db_get_i64 --- + + #[host("env")] + fn db_get_i64(iterator: i32, data_ptr: u32, data_len: u32) -> Result { + let (key, row_idx) = vm.iter_map[&iterator]; + let row = &vm.tables[&key].rows[row_idx]; + let size = row.data.len() as i32; + if data_len > 0 { + let copy_len = std::cmp::min(data_len as usize, row.data.len()); + write_buf(mem, data_ptr, &row.data[..copy_len]); + } + Ok(size) + } + + // --- Primary table: db_update_i64 --- + + #[host("env")] + fn db_update_i64(iterator: i32, _payer: u64, data_ptr: u32, data_len: u32) -> Result<(), Trap> { + let (key, row_idx) = vm.iter_map[&iterator]; + let data = read_buf(mem, data_ptr, data_len); + vm.tables.get_mut(&key).unwrap().rows[row_idx].data = data; + Ok(()) + } + + // --- Primary table: db_remove_i64 --- + + #[host("env")] + fn db_remove_i64(iterator: i32) -> Result<(), Trap> { + let (key, row_idx) = vm.iter_map[&iterator]; + vm.tables.get_mut(&key).unwrap().rows.remove(row_idx); + Ok(()) + } + + // --- Primary table: db_end_i64 --- + + #[host("env")] + fn db_end_i64(code: u64, scope: u64, table: u64) -> Result { + let key: TableKey = (code, scope, table); + match vm.tables.get(&key) { + Some(tbl) if !tbl.rows.is_empty() => { + Ok(vm.alloc_end_iter(key)) + } + _ => Ok(-1), + } + } + + // --- Primary table: db_previous_i64 --- + + #[host("env")] + fn db_previous_i64(iterator: i32, pk_ptr: u32) -> Result { + if iterator < -1 { + // End iterator — return last row + let key = vm.end_iter_map[&iterator]; + let tbl = &vm.tables[&key]; + if tbl.rows.is_empty() { + return Ok(-1); + } + let last_idx = tbl.rows.len() - 1; + let pk = tbl.rows[last_idx].pk; + write_buf(mem, pk_ptr, &pk.to_le_bytes()); + return Ok(vm.alloc_iter(key, last_idx)); + } + // Normal iterator — go to previous row + let (key, row_idx) = vm.iter_map[&iterator]; + if row_idx == 0 { + return Ok(-1); + } + let prev_idx = row_idx - 1; + let pk = vm.tables[&key].rows[prev_idx].pk; + write_buf(mem, pk_ptr, &pk.to_le_bytes()); + Ok(vm.alloc_iter(key, prev_idx)) + } + + // --- Primary table: db_next_i64 --- + + #[host("env")] + fn db_next_i64(iterator: i32, pk_ptr: u32) -> Result { + let (key, row_idx) = vm.iter_map[&iterator]; + let tbl = &vm.tables[&key]; + let next_idx = row_idx + 1; + if next_idx >= tbl.rows.len() { + return Ok(-1); + } + let pk = tbl.rows[next_idx].pk; + write_buf(mem, pk_ptr, &pk.to_le_bytes()); + Ok(vm.alloc_iter(key, next_idx)) + } + + // --- Primary table: db_lowerbound_i64 --- + + #[host("env")] + fn db_lowerbound_i64(code: u64, scope: u64, table: u64, id: u64) -> Result { + let key: TableKey = (code, scope, table); + if let Some(tbl) = vm.tables.get(&key) { + // Find first row with pk >= id + if let Some(pos) = tbl.rows.iter().position(|r| r.pk >= id) { + return Ok(vm.alloc_iter(key, pos)); + } + } + Ok(-1) + } + + // --- idx256 secondary index: store --- + + #[host("env")] + fn db_idx256_store(scope: u64, table: u64, payer: u64, id: u64, data_ptr: u32, _data_len: u32) -> Result { + let _ = payer; + let key: TableKey = (vm.receiver, scope, table); + let mut value = [0u8; 32]; + value.copy_from_slice(&read_buf(mem, data_ptr, 32)); + let tbl = vm.tables.entry(key).or_default(); + tbl.idx256.push(SecondaryIdx256 { pk: id, value, iter_id: 0 }); + let idx_pos = tbl.idx256.len() - 1; + Ok(vm.alloc_idx256_iter(key, idx_pos)) + } + + // --- idx256 secondary index: find --- + + #[host("env")] + fn db_idx256_find_secondary(code: u64, scope: u64, table: u64, data_ptr: u32, _data_len: u32, pk_ptr: u32) -> Result { + let key: TableKey = (code, scope, table); + let mut search_value = [0u8; 32]; + search_value.copy_from_slice(&read_buf(mem, data_ptr, 32)); + if let Some(tbl) = vm.tables.get(&key) { + if let Some(pos) = tbl.idx256.iter().position(|e| e.value == search_value) { + let pk = tbl.idx256[pos].pk; + write_buf(mem, pk_ptr, &pk.to_le_bytes()); + return Ok(vm.alloc_idx256_iter(key, pos)); + } + } + Ok(-1) + } + + // --- idx256 secondary index: update --- + + #[host("env")] + fn db_idx256_update(iterator: i32, _payer: u64, data_ptr: u32, _data_len: u32) -> Result<(), Trap> { + let (key, idx_pos) = vm.idx256_iter_map[&iterator]; + let mut value = [0u8; 32]; + value.copy_from_slice(&read_buf(mem, data_ptr, 32)); + vm.tables.get_mut(&key).unwrap().idx256[idx_pos].value = value; + Ok(()) + } + + // --- idx256 secondary index: remove --- + + #[host("env")] + fn db_idx256_remove(iterator: i32) -> Result<(), Trap> { + let (key, idx_pos) = vm.idx256_iter_map[&iterator]; + vm.tables.get_mut(&key).unwrap().idx256.remove(idx_pos); + Ok(()) + } + + // --- idx64 secondary index (stubs for compilation — not deeply tested yet) --- + + #[host("env")] + fn db_idx64_find_secondary(code: u64, scope: u64, table: u64, _secondary_ptr: u32, _primary_ptr: u32) -> Result { + let _key: TableKey = (code, scope, table); + Ok(-1) // not found — stub + } + + #[host("env")] + fn db_idx64_lowerbound(code: u64, scope: u64, table: u64, _secondary_ptr: u32, _primary_ptr: u32) -> Result { + let _key: TableKey = (code, scope, table); + Ok(-1) // not found — stub + } + + // --- idx128 secondary index (stubs) --- + + #[host("env")] + fn db_idx128_find_secondary(code: u64, scope: u64, table: u64, _secondary_ptr: u32, _primary_ptr: u32) -> Result { + let _key: TableKey = (code, scope, table); + Ok(-1) + } + + #[host("env")] + fn db_idx128_lowerbound(code: u64, scope: u64, table: u64, _secondary_ptr: u32, _primary_ptr: u32) -> Result { + let _key: TableKey = (code, scope, table); + Ok(-1) + } + + // --- idx256 lowerbound --- + + #[host("env")] + fn db_idx256_lowerbound(code: u64, scope: u64, table: u64, _data_ptr: u32, _data_len: u32, _primary_ptr: u32) -> Result { + let _key: TableKey = (code, scope, table); + Ok(-1) // stub + } + + // --- Standard C library functions (imported by some WASM modules) --- + + #[host("env")] + fn memcpy(dest: u32, src: u32, len: u32) -> Result { + let data = mem[src as usize..(src + len) as usize].to_vec(); + mem[dest as usize..(dest + len) as usize].copy_from_slice(&data); + Ok(dest) + } + + #[host("env")] + fn memmove(dest: u32, src: u32, len: u32) -> Result { + let data = mem[src as usize..(src + len) as usize].to_vec(); + mem[dest as usize..(dest + len) as usize].copy_from_slice(&data); + Ok(dest) + } + + #[host("env")] + fn memset(dest: u32, val: u32, len: u32) -> Result { + let byte = val as u8; + for i in 0..len as usize { + mem[dest as usize + i] = byte; + } + Ok(dest) + } + + #[host("env")] + fn memcmp(s1: u32, s2: u32, len: u32) -> Result { + for i in 0..len as usize { + let a = mem[s1 as usize + i]; + let b = mem[s2 as usize + i]; + if a != b { + return Ok(if a < b { -1 } else { 1 }); + } + } + Ok(0) + } +} + +// ─── MockAntelope ─── + +pub struct MockAntelope { + wasm: Vec, + runtime: Runtime, +} + +impl MockAntelope { + /// Compile Solidity source to Antelope WASM and prepare a mock runtime. + pub fn build(src: &str) -> Self { + let wasm = build_antelope_wasm(src); + let receiver = string_to_name("testaccount"); + Self { + wasm, + runtime: Runtime::new(receiver), + } + } + + /// Call an action by name. `data` is the DataStream-encoded action parameters. + /// Receiver == code (direct call, not notification). + pub fn action(&mut self, action_name: &str, data: Vec) { + let action_encoded = string_to_name(action_name) as i64; + let receiver = self.runtime.receiver as i64; + self.runtime.action_data = data; + self.runtime.prints.clear(); + self.runtime.inline_actions.clear(); + self.runtime.notifications.clear(); + self.execute_apply(receiver, receiver, action_encoded) + .expect("action should not trap"); + } + + /// Call an action, expecting it to trap/revert. + pub fn action_expect_failure(&mut self, action_name: &str, data: Vec) { + let action_encoded = string_to_name(action_name) as i64; + let receiver = self.runtime.receiver as i64; + self.runtime.action_data = data; + self.runtime.prints.clear(); + self.runtime.inline_actions.clear(); + self.runtime.notifications.clear(); + match self.execute_apply(receiver, receiver, action_encoded) { + Err(_) => (), // Expected failure + Ok(_) => panic!("expected action to fail, but it succeeded"), + } + } + + /// Call as a notification: code != receiver. + /// This simulates receiving an inline action from another contract. + pub fn notification(&mut self, code: u64, action_name: &str, data: Vec) { + let action_encoded = string_to_name(action_name) as i64; + let receiver = self.runtime.receiver as i64; + self.runtime.action_data = data; + self.runtime.prints.clear(); + self.runtime.inline_actions.clear(); + self.runtime.notifications.clear(); + self.execute_apply(receiver, code as i64, action_encoded) + .expect("notification should not trap"); + } + + /// Get the accumulated print output from the last action call. + pub fn prints(&self) -> &str { + &self.runtime.prints + } + + /// Set authorized accounts for subsequent actions. + pub fn set_auth(&mut self, accounts: Vec) { + self.runtime.auth_accounts = accounts; + } + + /// Get inline actions sent during the last call. + pub fn inline_actions(&self) -> &[Vec] { + &self.runtime.inline_actions + } + + /// Get the raw table storage. + pub fn tables(&self) -> &HashMap { + &self.runtime.tables + } + + fn execute_apply(&mut self, receiver: i64, code: i64, action: i64) -> Result<(), Error> { + let engine = Engine::default(); + let mut store = Store::new(&engine, std::mem::replace( + &mut self.runtime, + Runtime::new(receiver as u64), + )); + // Restore actual receiver + store.data_mut().receiver = receiver as u64; + + let mut linker = Linker::new(&engine); + + // Define all host functions (must come before instantiation) + Runtime::define(&mut store, &mut linker); + + let module = Module::new(&engine, &self.wasm[..]).expect("WASM should be valid"); + let instance = linker + .instantiate(&mut store, &module) + .expect("instantiation should succeed") + .ensure_no_start(&mut store) + .expect("no start function expected"); + + // Get the exported memory (WASM defines its own memory and exports it) + let memory = instance + .get_export(&store, "memory") + .and_then(|e| e.into_memory()) + .expect("memory export should exist"); + store.data_mut().memory = Some(memory); + + // Call apply(receiver, code, action) + let apply = instance + .get_export(&store, "apply") + .and_then(|e| e.into_func()) + .expect("apply export should exist"); + + let args = [ + Value::I64(receiver), + Value::I64(code), + Value::I64(action), + ]; + let result = apply.call(&mut store, &args, &mut []); + + // Transfer state back + self.runtime = store.into_data(); + + result + } +} + +// ─── Compilation ─── + +fn build_antelope_wasm(src: &str) -> Vec { + let tmp_file = OsStr::new("test.sol"); + let mut cache = FileResolver::default(); + cache.set_file_contents(tmp_file.to_str().unwrap(), src.to_string()); + let (wasm, ns) = compile( + tmp_file, + &mut cache, + Target::Antelope, + &Options { + opt_level: inkwell::OptimizationLevel::Default.into(), + ..Default::default() + }, + vec!["test".to_string()], + "0.0.1", + ); + ns.print_diagnostics_in_plain(&cache, false); + assert!(!wasm.is_empty(), "compilation should produce WASM output"); + // wasm is Vec<(Vec, String)> — first element is (wasm_bytes, abi_json) + wasm[0].0.clone() +} + +/// Helper: build a MockAntelope from Solidity source +pub fn build_solidity(src: &str) -> MockAntelope { + MockAntelope::build(src) +} diff --git a/tests/antelope_tests/builtins.rs b/tests/antelope_tests/builtins.rs new file mode 100644 index 000000000..0d7069903 --- /dev/null +++ b/tests/antelope_tests/builtins.rs @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{build_solidity, encode_action_data, string_to_name, ActionParam}; + +#[test] +fn self_returns_receiver() { + let mut vm = build_solidity( + r#" + contract Test { + function whoami(uint64 expected) public { + uint64 me = antelope.self(); + if (me == expected) { + print("match"); + } else { + print("mismatch"); + } + } + } + "#, + ); + + let receiver = string_to_name("testaccount"); + let data = encode_action_data(&[ActionParam::U64(receiver)]); + vm.action("whoami", data); + assert_eq!(vm.prints(), "match"); + + // Wrong value should not match + let wrong = string_to_name("alice"); + let data = encode_action_data(&[ActionParam::U64(wrong)]); + vm.action("whoami", data); + assert_eq!(vm.prints(), "mismatch"); +} + +#[test] +fn timestamp_returns_value() { + let mut vm = build_solidity( + r#" + contract Test { + function checktime(uint64 expected) public { + uint64 t = antelope.timestamp(); + if (t == expected) { + print("match"); + } else { + print("mismatch"); + } + } + } + "#, + ); + + // Mock returns 1704067200_000_000 (2024-01-01T00:00:00Z in microseconds) + let data = encode_action_data(&[ActionParam::U64(1704067200_000_000)]); + vm.action("checktime", data); + assert_eq!(vm.prints(), "match"); + + // Wrong value should not match + let data = encode_action_data(&[ActionParam::U64(0)]); + vm.action("checktime", data); + assert_eq!(vm.prints(), "mismatch"); +} + +#[test] +fn require_auth2_pass() { + let mut vm = build_solidity( + r#" + contract Test { + function check(uint64 account, uint64 perm) public { + antelope.requireAuth2(account, perm); + print("ok"); + } + } + "#, + ); + + let receiver = string_to_name("testaccount"); + let active = string_to_name("active"); + vm.set_auth(vec![receiver]); + let data = encode_action_data(&[ActionParam::U64(receiver), ActionParam::U64(active)]); + vm.action("check", data); + assert_eq!(vm.prints(), "ok"); +} + +#[test] +fn require_auth2_fail() { + let mut vm = build_solidity( + r#" + contract Test { + function check(uint64 account, uint64 perm) public { + antelope.requireAuth2(account, perm); + } + } + "#, + ); + + let alice = string_to_name("alice"); + let active = string_to_name("active"); + let receiver = string_to_name("testaccount"); + vm.set_auth(vec![receiver]); + let data = encode_action_data(&[ActionParam::U64(alice), ActionParam::U64(active)]); + vm.action_expect_failure("check", data); +} + +#[test] +fn multiple_actions_independent_prints() { + let mut vm = build_solidity( + r#" + contract Test { + function alpha() public { + print("a"); + } + function beta() public { + print("b"); + } + } + "#, + ); + + vm.action("alpha", vec![]); + assert_eq!(vm.prints(), "a"); + + vm.action("beta", vec![]); + assert_eq!(vm.prints(), "b", "prints should be cleared between actions"); +} + +#[test] +fn name_builtin_compile_time() { + let mut vm = build_solidity( + r#" + contract Test { + function checkname(uint64 expected) public { + uint64 alice = antelope.name("alice"); + if (alice == expected) { + print("match"); + } else { + print("mismatch"); + } + } + } + "#, + ); + + // antelope.name("alice") should equal string_to_name("alice") + let alice = string_to_name("alice"); + let data = encode_action_data(&[ActionParam::U64(alice)]); + vm.action("checkname", data); + assert_eq!(vm.prints(), "match"); + + // Wrong value should not match + let data = encode_action_data(&[ActionParam::U64(0)]); + vm.action("checkname", data); + assert_eq!(vm.prints(), "mismatch"); +} + +#[test] +fn code_returns_code_param() { + let mut vm = build_solidity( + r#" + contract Test { + function checkcode(uint64 expected) public { + uint64 c = antelope.code(); + if (c == expected) { + print("match"); + } else { + print("mismatch"); + } + } + } + "#, + ); + + // Direct call: code == receiver + let receiver = string_to_name("testaccount"); + let data = encode_action_data(&[ActionParam::U64(receiver)]); + vm.action("checkcode", data); + assert_eq!(vm.prints(), "match"); +} + +#[test] +fn notification_dispatches_with_different_code() { + let mut vm = build_solidity( + r#" + contract Test { + function transfer() public { + uint64 me = antelope.self(); + uint64 c = antelope.code(); + if (me != c) { + print("notification"); + } else { + print("direct"); + } + } + } + "#, + ); + + // Direct call: code == receiver + vm.action("transfer", vec![]); + assert_eq!(vm.prints(), "direct"); + + // Notification: code != receiver + let eosio_token = string_to_name("eosio.token"); + vm.notification(eosio_token, "transfer", vec![]); + assert_eq!(vm.prints(), "notification"); +} diff --git a/tests/antelope_tests/events.rs b/tests/antelope_tests/events.rs new file mode 100644 index 000000000..f8939a5c4 --- /dev/null +++ b/tests/antelope_tests/events.rs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{build_solidity, encode_action_data, ActionParam}; + +#[test] +fn event_with_multiple_fields() { + let mut vm = build_solidity( + r#" + contract Test { + event Transfer(uint64 from, uint64 to, uint64 amount); + + function send(uint64 from, uint64 to, uint64 amount) public { + emit Transfer(from, to, amount); + } + } + "#, + ); + + let data = encode_action_data(&[ + ActionParam::U64(1), + ActionParam::U64(2), + ActionParam::U64(100), + ]); + vm.action("send", data); + assert_eq!(vm.inline_actions().len(), 1, "should produce exactly one inline action"); +} + +#[test] +fn multiple_events_in_one_action() { + let mut vm = build_solidity( + r#" + contract Test { + event Step(uint64 n); + + function multistep() public { + emit Step(1); + emit Step(2); + emit Step(3); + } + } + "#, + ); + + vm.action("multistep", vec![]); + assert_eq!(vm.inline_actions().len(), 3, "should produce three inline actions"); +} + +#[test] +fn event_and_print_together() { + let mut vm = build_solidity( + r#" + contract Test { + event Done(uint64 val); + + function work(uint64 v) public { + print("working"); + emit Done(v); + } + } + "#, + ); + + let data = encode_action_data(&[ActionParam::U64(42)]); + vm.action("work", data); + assert_eq!(vm.prints(), "working"); + assert_eq!(vm.inline_actions().len(), 1); +} + +#[test] +fn no_event_no_inline_action() { + let mut vm = build_solidity( + r#" + contract Test { + function noop() public { + print("nothing"); + } + } + "#, + ); + + vm.action("noop", vec![]); + assert!(vm.inline_actions().is_empty(), "no events means no inline actions"); +} diff --git a/tests/antelope_tests/first.rs b/tests/antelope_tests/first.rs new file mode 100644 index 000000000..57ffcd518 --- /dev/null +++ b/tests/antelope_tests/first.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{build_solidity, encode_action_data, string_to_name, ActionParam}; + +#[test] +fn hello_world() { + let mut vm = build_solidity( + r#" + contract Hello { + function hi() public { + print("Hello, Antelope!"); + } + } + "#, + ); + + vm.action("hi", vec![]); + assert_eq!(vm.prints(), "Hello, Antelope!"); +} + +#[test] +fn counter_increment() { + let mut vm = build_solidity( + r#" + contract Counter { + uint64 count; + + function increment() public { + count += 1; + } + + function check(uint64 expected) public { + if (count == expected) { + print("match"); + } else { + print("mismatch"); + } + } + } + "#, + ); + + vm.action("increment", vec![]); + let data = encode_action_data(&[ActionParam::U64(1)]); + vm.action("check", data); + assert_eq!(vm.prints(), "match"); + + vm.action("increment", vec![]); + vm.action("increment", vec![]); + let data = encode_action_data(&[ActionParam::U64(3)]); + vm.action("check", data); + assert_eq!(vm.prints(), "match"); + + // Verify wrong value doesn't match + let data = encode_action_data(&[ActionParam::U64(99)]); + vm.action("check", data); + assert_eq!(vm.prints(), "mismatch"); +} + +#[test] +fn string_param() { + let mut vm = build_solidity( + r#" + contract Greeter { + function greet(string memory who) public { + print(who); + } + } + "#, + ); + + let data = encode_action_data(&[ActionParam::String("Alice".to_string())]); + vm.action("greet", data); + assert_eq!(vm.prints(), "Alice"); +} + +#[test] +fn uint64_param() { + let mut vm = build_solidity( + r#" + contract Test { + uint64 public stored; + + function setval(uint64 val) public { + stored = val; + } + + function check(uint64 expected) public { + if (stored == expected) { + print("match"); + } else { + print("mismatch"); + } + } + } + "#, + ); + + let data = encode_action_data(&[ActionParam::U64(42)]); + vm.action("setval", data); + + let data = encode_action_data(&[ActionParam::U64(42)]); + vm.action("check", data); + assert_eq!(vm.prints(), "match"); + + let data = encode_action_data(&[ActionParam::U64(99)]); + vm.action("check", data); + assert_eq!(vm.prints(), "mismatch"); +} + +#[test] +fn require_auth_pass() { + let mut vm = build_solidity( + r#" + contract Test { + function authme(uint64 account) public { + antelope.requireAuth(account); + print("authorized"); + } + } + "#, + ); + + let receiver = string_to_name("testaccount"); + vm.set_auth(vec![receiver]); + let data = encode_action_data(&[ActionParam::U64(receiver)]); + vm.action("authme", data); + assert_eq!(vm.prints(), "authorized"); +} + +#[test] +fn require_auth_fail() { + let mut vm = build_solidity( + r#" + contract Test { + function authme(uint64 account) public { + antelope.requireAuth(account); + } + } + "#, + ); + + let alice = string_to_name("alice"); + let receiver = string_to_name("testaccount"); + vm.set_auth(vec![receiver]); + let data = encode_action_data(&[ActionParam::U64(alice)]); + vm.action_expect_failure("authme", data); +} + +#[test] +fn has_auth_check() { + let mut vm = build_solidity( + r#" + contract Test { + function check(uint64 account) public { + if (antelope.hasAuth(account)) { + print("yes"); + } else { + print("no"); + } + } + } + "#, + ); + + let receiver = string_to_name("testaccount"); + let alice = string_to_name("alice"); + vm.set_auth(vec![receiver]); + + let data = encode_action_data(&[ActionParam::U64(receiver)]); + vm.action("check", data); + assert_eq!(vm.prints(), "yes"); + + let data = encode_action_data(&[ActionParam::U64(alice)]); + vm.action("check", data); + assert_eq!(vm.prints(), "no"); +} + +#[test] +fn emit_event_sends_inline_action() { + let mut vm = build_solidity( + r#" + contract Test { + event Ping(uint64 value); + + function doping(uint64 val) public { + emit Ping(val); + } + } + "#, + ); + + let data = encode_action_data(&[ActionParam::U64(123)]); + vm.action("doping", data); + assert!( + !vm.inline_actions().is_empty(), + "event should produce an inline action" + ); +} diff --git a/tests/antelope_tests/mod.rs b/tests/antelope_tests/mod.rs new file mode 100644 index 000000000..8ca28ce3a --- /dev/null +++ b/tests/antelope_tests/mod.rs @@ -0,0 +1,4 @@ +mod builtins; +mod events; +mod first; +mod storage; diff --git a/tests/antelope_tests/storage.rs b/tests/antelope_tests/storage.rs new file mode 100644 index 000000000..7345bbda5 --- /dev/null +++ b/tests/antelope_tests/storage.rs @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{build_solidity, encode_action_data, string_to_name, ActionParam}; + +#[test] +fn mapping_store_and_load() { + let mut vm = build_solidity( + r#" + contract Test { + mapping(uint64 => uint64) public data; + + function store(uint64 key, uint64 val) public { + data[key] = val; + } + + function check(uint64 key, uint64 expected) public { + if (data[key] == expected) { + print("match"); + } else { + print("mismatch"); + } + } + } + "#, + ); + + // Store a value + let data = encode_action_data(&[ActionParam::U64(1), ActionParam::U64(42)]); + vm.action("store", data); + + // Read back the exact value + let data = encode_action_data(&[ActionParam::U64(1), ActionParam::U64(42)]); + vm.action("check", data); + assert_eq!(vm.prints(), "match"); + + // Wrong value should not match + let data = encode_action_data(&[ActionParam::U64(1), ActionParam::U64(99)]); + vm.action("check", data); + assert_eq!(vm.prints(), "mismatch"); + + // Non-existent key should read as 0 + let data = encode_action_data(&[ActionParam::U64(999), ActionParam::U64(0)]); + vm.action("check", data); + assert_eq!(vm.prints(), "match"); +} + +#[test] +fn storage_overwrite() { + let mut vm = build_solidity( + r#" + contract Test { + uint64 public val; + + function setval(uint64 v) public { + val = v; + } + + function check(uint64 expected) public { + if (val == expected) { + print("match"); + } else { + print("mismatch"); + } + } + } + "#, + ); + + let data = encode_action_data(&[ActionParam::U64(10)]); + vm.action("setval", data); + let rows_after_first: usize = vm.tables().values().map(|t| t.rows.len()).sum(); + + // Verify value is 10 + let data = encode_action_data(&[ActionParam::U64(10)]); + vm.action("check", data); + assert_eq!(vm.prints(), "match"); + + // Overwrite with 20 + let data = encode_action_data(&[ActionParam::U64(20)]); + vm.action("setval", data); + let rows_after_second: usize = vm.tables().values().map(|t| t.rows.len()).sum(); + + // Row count shouldn't grow (update path, not insert) + assert_eq!(rows_after_first, rows_after_second, "overwrite should not add rows"); + + // Verify value is now 20, not 10 + let data = encode_action_data(&[ActionParam::U64(20)]); + vm.action("check", data); + assert_eq!(vm.prints(), "match"); + + let data = encode_action_data(&[ActionParam::U64(10)]); + vm.action("check", data); + assert_eq!(vm.prints(), "mismatch"); +} + +#[test] +fn multiple_state_vars() { + let mut vm = build_solidity( + r#" + contract Test { + uint64 public a; + uint64 public b; + + function setboth(uint64 va, uint64 vb) public { + a = va; + b = vb; + } + } + "#, + ); + + let data = encode_action_data(&[ActionParam::U64(100), ActionParam::U64(200)]); + vm.action("setboth", data); + + // Both variables stored — should have table rows + let total_rows: usize = vm.tables().values().map(|t| t.rows.len()).sum(); + assert!(total_rows >= 2, "should have at least 2 storage rows for 2 variables"); +} + +#[test] +fn string_storage() { + let mut vm = build_solidity( + r#" + contract Test { + string public stored; + + function save(string memory s) public { + stored = s; + } + + function read() public { + print(stored); + } + } + "#, + ); + + let data = encode_action_data(&[ActionParam::String("hello world".to_string())]); + vm.action("save", data); + + // Read back the exact string + vm.action("read", vec![]); + assert_eq!(vm.prints(), "hello world"); +} + +#[test] +fn bool_storage() { + let mut vm = build_solidity( + r#" + contract Test { + bool public flag; + + function setflag(bool v) public { + flag = v; + } + + function readflag() public { + if (flag) { + print("true"); + } else { + print("false"); + } + } + } + "#, + ); + + // Set true and read back + let data = encode_action_data(&[ActionParam::Bool(true)]); + vm.action("setflag", data); + vm.action("readflag", vec![]); + assert_eq!(vm.prints(), "true"); + + // Set false and read back — verifies overwrite works for bools + let data = encode_action_data(&[ActionParam::Bool(false)]); + vm.action("setflag", data); + vm.action("readflag", vec![]); + assert_eq!(vm.prints(), "false"); +} + +#[test] +fn checked_arithmetic_overflow_reverts() { + let mut vm = build_solidity( + r#" + contract Test { + function add(uint64 a, uint64 b) public { + uint64 c = a + b; + // If we get here, no overflow + if (c > 0) { + print("ok"); + } + } + } + "#, + ); + + // Normal addition should succeed + let data = encode_action_data(&[ActionParam::U64(10), ActionParam::U64(20)]); + vm.action("add", data); + assert_eq!(vm.prints(), "ok"); + + // Overflow: max_uint64 + 1 should revert + let data = encode_action_data(&[ActionParam::U64(u64::MAX), ActionParam::U64(1)]); + vm.action_expect_failure("add", data); +} + +#[test] +fn checked_arithmetic_underflow_reverts() { + let mut vm = build_solidity( + r#" + contract Test { + function sub(uint64 a, uint64 b) public { + uint64 c = a - b; + if (c == 0) { + print("zero"); + } else { + print("nonzero"); + } + } + } + "#, + ); + + // Normal subtraction + let data = encode_action_data(&[ActionParam::U64(10), ActionParam::U64(10)]); + vm.action("sub", data); + assert_eq!(vm.prints(), "zero"); + + // Underflow: 5 - 10 should revert + let data = encode_action_data(&[ActionParam::U64(5), ActionParam::U64(10)]); + vm.action_expect_failure("sub", data); +} + +#[test] +fn delete_mapping_entry() { + let mut vm = build_solidity( + r#" + contract Test { + mapping(uint64 => uint64) public data; + + function store(uint64 key, uint64 val) public { + data[key] = val; + } + + function remove(uint64 key) public { + delete data[key]; + } + + function load(uint64 key) public { + if (data[key] != 0) { + print("found"); + } else { + print("empty"); + } + } + } + "#, + ); + + // Store a value + let data = encode_action_data(&[ActionParam::U64(1), ActionParam::U64(42)]); + vm.action("store", data); + + // Verify it's there + let data = encode_action_data(&[ActionParam::U64(1)]); + vm.action("load", data); + assert_eq!(vm.prints(), "found"); + + // Delete it + let data = encode_action_data(&[ActionParam::U64(1)]); + vm.action("remove", data); + + // Verify it's gone + let data = encode_action_data(&[ActionParam::U64(1)]); + vm.action("load", data); + assert_eq!(vm.prints(), "empty"); +} diff --git a/tests/contract.rs b/tests/contract.rs index 65c2ccb65..69cf8a9aa 100644 --- a/tests/contract.rs +++ b/tests/contract.rs @@ -36,6 +36,11 @@ fn evm_contracts() -> io::Result<()> { contract_tests("tests/contract_testcases/evm", Target::EVM) } +#[test] +fn antelope_contracts() -> io::Result<()> { + contract_tests("tests/contract_testcases/antelope", Target::Antelope) +} + fn contract_tests(file_path: &str, target: Target) -> io::Result<()> { let path = PathBuf::from(file_path); recurse_directory(path, target) @@ -98,7 +103,7 @@ fn parse_file(path: PathBuf, target: Target) -> io::Result<()> { if contract.instantiable { let code = match ns.target { - Target::Solana | Target::Polkadot { .. } => { + Target::Solana | Target::Polkadot { .. } | Target::Antelope => { contract.emit(&ns, &Default::default(), contract_no) } Target::EVM => b"beep".to_vec(), diff --git a/tests/contract_testcases/antelope/builtins/auth.sol b/tests/contract_testcases/antelope/builtins/auth.sol new file mode 100644 index 000000000..a8df8a250 --- /dev/null +++ b/tests/contract_testcases/antelope/builtins/auth.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract AuthTest { + function doAuth(uint64 account) public { + antelope.requireAuth(account); + } + + function checkAuth(uint64 account) public returns (bool) { + return antelope.hasAuth(account); + } + + function doAuth2(uint64 account, uint64 permission) public { + antelope.requireAuth2(account, permission); + } +} +// ---- Expect: diagnostics ---- +// warning: 5:5-43: function can be declared 'pure' +// warning: 9:5-61: function can be declared 'pure' +// warning: 13:5-63: function can be declared 'pure' diff --git a/tests/contract_testcases/antelope/builtins/cross_contract.sol b/tests/contract_testcases/antelope/builtins/cross_contract.sol new file mode 100644 index 000000000..53707bb2a --- /dev/null +++ b/tests/contract_testcases/antelope/builtins/cross_contract.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract CrossContract { + function sendTransfer(uint64 from, uint64 to, int64 amount, string memory memo) public { + bytes memory packed = antelope.pack(from, to, amount, antelope.name("EOS"), memo); + antelope.call(antelope.name("eosio.token"), antelope.name("transfer"), packed); + } + + function sendWithAuth(uint64 from, uint64 to, int64 amount, string memory memo) public { + bytes memory packed = antelope.pack(from, to, amount, antelope.name("EOS"), memo); + antelope.callauth( + antelope.name("eosio.token"), + antelope.name("transfer"), + packed, + from, + antelope.name("active") + ); + } + + function notify(uint64 account) public { + antelope.requireRecipient(account); + } +} +// ---- Expect: diagnostics ---- +// warning: 5:5-91: function can be declared 'pure' +// warning: 10:5-91: function can be declared 'pure' +// warning: 21:5-43: function can be declared 'pure' diff --git a/tests/contract_testcases/antelope/builtins/identity.sol b/tests/contract_testcases/antelope/builtins/identity.sol new file mode 100644 index 000000000..3d204feef --- /dev/null +++ b/tests/contract_testcases/antelope/builtins/identity.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract IdentityTest { + uint64 public lastSelf; + uint64 public lastCode; + uint64 public lastTime; + + function checkIdentity() public { + lastSelf = antelope.self(); + lastCode = antelope.code(); + lastTime = antelope.timestamp(); + } + + function nameTest() public returns (uint64) { + return antelope.name("eosio.token"); + } +} +// ---- Expect: diagnostics ---- +// warning: 15:5-48: function can be declared 'pure' diff --git a/tests/contract_testcases/antelope/builtins/table_access.sol b/tests/contract_testcases/antelope/builtins/table_access.sol new file mode 100644 index 000000000..07deedf57 --- /dev/null +++ b/tests/contract_testcases/antelope/builtins/table_access.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract TableAccessTest { + function readBalance(uint64 account, uint64 symbolCode) public returns (int64) { + int32 iter = antelope.dbFind( + antelope.name("eosio.token"), + account, + antelope.name("accounts"), + symbolCode + ); + if (iter < 0) return 0; + bytes memory row = antelope.dbGet(iter); + return antelope.toInt64(row, 0); + } + + function countRows(uint64 code, uint64 scope, uint64 table) public returns (uint64) { + uint64 count = 0; + int32 iter = antelope.dbLowerbound(code, scope, table, 0); + while (iter >= 0) { + count += 1; + iter = antelope.dbNext(iter); + } + return count; + } + + function findByIdx64(uint64 code, uint64 scope, uint64 table, uint64 indexNum, uint64 key) public returns (uint64) { + int32 secIter = antelope.dbIdx64Find(code, scope, table, indexNum, key); + if (secIter < 0) return 0; + return antelope.lastPk(); + } + + function decodeRow(bytes memory data) public pure returns (uint64, uint32, string memory) { + uint64 val64 = antelope.toUint64(data, 0); + uint32 val32 = antelope.toUint32(data, 8); + string memory s = antelope.toString(data, 12); + return (val64, val32, s); + } +} +// ---- Expect: diagnostics ---- +// warning: 5:5-83: function can be declared 'pure' +// warning: 17:5-88: function can be declared 'pure' +// warning: 27:5-119: function can be declared 'pure' diff --git a/tests/contract_testcases/antelope/errors/assembly.sol b/tests/contract_testcases/antelope/errors/assembly.sol new file mode 100644 index 000000000..31bc8da84 --- /dev/null +++ b/tests/contract_testcases/antelope/errors/assembly.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract AssemblyTest { + function doAsm() public { + assembly { + let x := 1 + } + } +} +// ---- Expect: diagnostics ---- +// error: 6:9-8:10: inline assembly is not supported on Antelope. Use antelope.* builtins instead. diff --git a/tests/contract_testcases/antelope/errors/constructor.sol b/tests/contract_testcases/antelope/errors/constructor.sol new file mode 100644 index 000000000..cdf2c0879 --- /dev/null +++ b/tests/contract_testcases/antelope/errors/constructor.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract WithConstructor { + uint64 public value; + + constructor() { + value = 42; + } + + function getValue() public returns (uint64) { + return value; + } +} +// ---- Expect: diagnostics ---- +// error: 7:5-19: constructors are not supported on Antelope. Use an explicit init() action instead. diff --git a/tests/contract_testcases/antelope/errors/payable.sol b/tests/contract_testcases/antelope/errors/payable.sol new file mode 100644 index 000000000..2b96438f5 --- /dev/null +++ b/tests/contract_testcases/antelope/errors/payable.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract PayableTest { + function deposit() public payable { + } +} +// ---- Expect: diagnostics ---- +// error: 5:5-38: Antelope does not support payable functions. Use explicit token transfer actions instead. diff --git a/tests/contract_testcases/antelope/first/counter.sol b/tests/contract_testcases/antelope/first/counter.sol new file mode 100644 index 000000000..36c9daf0c --- /dev/null +++ b/tests/contract_testcases/antelope/first/counter.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract Counter { + uint64 public count; + + function increment() public { + count += 1; + } + + function reset() public { + count = 0; + } + + function getCount() public returns (uint64) { + return count; + } +} +// ---- Expect: diagnostics ---- +// warning: 15:5-48: function can be declared 'view' diff --git a/tests/contract_testcases/antelope/first/hello.sol b/tests/contract_testcases/antelope/first/hello.sol new file mode 100644 index 000000000..416e9e8d7 --- /dev/null +++ b/tests/contract_testcases/antelope/first/hello.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract Hello { + function hi() public { + print("Hello, Antelope!"); + } +} +// ---- Expect: diagnostics ---- +// warning: 5:5-25: function can be declared 'pure' diff --git a/tests/contract_testcases/antelope/first/with_string.sol b/tests/contract_testcases/antelope/first/with_string.sol new file mode 100644 index 000000000..79e158ba0 --- /dev/null +++ b/tests/contract_testcases/antelope/first/with_string.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract WithString { + string public name; + + function setName(string memory newName) public { + name = newName; + } + + function greet(string memory who) public { + print(who); + } +} +// ---- Expect: diagnostics ---- +// warning: 11:5-45: function can be declared 'pure' diff --git a/tests/contract_testcases/antelope/storage/erc20.sol b/tests/contract_testcases/antelope/storage/erc20.sol new file mode 100644 index 000000000..299950b4e --- /dev/null +++ b/tests/contract_testcases/antelope/storage/erc20.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract ERC20 { + string public name; + string public symbol; + uint256 public totalSupply; + uint64 public owner; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + function init(string memory _name, string memory _symbol, uint64 _owner) public { + antelope.requireAuth(_owner); + name = _name; + symbol = _symbol; + owner = _owner; + } + + function mint(uint64 actor, address to, uint256 amount) public { + antelope.requireAuth(actor); + balanceOf[to] += amount; + totalSupply += amount; + emit Transfer(address(0), to, amount); + } + + function transfer(uint64 actor, address from, address to, uint256 amount) public { + antelope.requireAuth(actor); + require(balanceOf[from] >= amount, "insufficient balance"); + balanceOf[from] -= amount; + balanceOf[to] += amount; + emit Transfer(from, to, amount); + } + + function approve(uint64 actor, address _owner, address spender, uint256 amount) public { + antelope.requireAuth(actor); + allowance[_owner][spender] = amount; + emit Approval(_owner, spender, amount); + } +} +// ---- Expect: diagnostics ---- diff --git a/tests/contract_testcases/antelope/storage/mapping.sol b/tests/contract_testcases/antelope/storage/mapping.sol new file mode 100644 index 000000000..b95ff9be8 --- /dev/null +++ b/tests/contract_testcases/antelope/storage/mapping.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract MappingTest { + mapping(uint64 => uint256) public balances; + + function set(uint64 key, uint256 value) public { + balances[key] = value; + } + + function get(uint64 key) public returns (uint256) { + return balances[key]; + } + + function remove(uint64 key) public { + delete balances[key]; + } +} +// ---- Expect: diagnostics ---- +// warning: 11:5-54: function can be declared 'view' diff --git a/tests/contract_testcases/antelope/storage/nested_mapping.sol b/tests/contract_testcases/antelope/storage/nested_mapping.sol new file mode 100644 index 000000000..4714e72f2 --- /dev/null +++ b/tests/contract_testcases/antelope/storage/nested_mapping.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract NestedMapping { + mapping(address => mapping(address => uint256)) public allowances; + + function approve(address owner, address spender, uint256 amount) public { + allowances[owner][spender] = amount; + } + + function getAllowance(address owner, address spender) public returns (uint256) { + return allowances[owner][spender]; + } +} +// ---- Expect: diagnostics ---- +// warning: 11:5-83: function can be declared 'view' diff --git a/tests/wasm_host_attr/src/lib.rs b/tests/wasm_host_attr/src/lib.rs index f0d51bbb6..7b7087815 100644 --- a/tests/wasm_host_attr/src/lib.rs +++ b/tests/wasm_host_attr/src/lib.rs @@ -3,7 +3,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens}; -use syn::{ImplItem, ItemImpl, LitInt, Type}; +use syn::{ImplItem, ItemImpl, LitInt, LitStr, Type}; struct HostFn { name: String, @@ -20,11 +20,16 @@ impl HostFn { _ => return None, // Only care about functions }; - let module = item - .attrs - .iter() - .find(|attr| attr.path().get_ident().unwrap() == "seal") - .map(|attr| format!("seal{}", attr.parse_args::().unwrap()))?; + let module = item.attrs.iter().find_map(|attr| { + let ident = attr.path().get_ident()?; + if ident == "seal" { + Some(format!("seal{}", attr.parse_args::().unwrap())) + } else if ident == "host" { + Some(attr.parse_args::().unwrap().value()) + } else { + None + } + })?; Some(HostFn { name: item.sig.ident.to_string(), @@ -59,7 +64,8 @@ impl HostFn { /// Helper macro for creating wasmi host function wrappers. /// Should be used on a dedicated impl block on the host state type. /// -/// Wraps functions with the `[seal(n)]` attribute, where n is the version number, into a wasmi host function. +/// Wraps functions with the `[seal(n)]` attribute (module "seal{n}") or `[host("module")]` +/// attribute (custom module name) into a wasmi host function. /// The function signature should match exactly the signature of the closure going into [`Func::wrap`][1]. /// There will be two local variables brought into scope: /// * `mem` for accessing the memory