Skip to content

Align compiler with unified VM opcodes - #63

Merged
msinkec merged 9 commits into
masterfrom
agent/update-vm-opcodes
Jul 28, 2026
Merged

Align compiler with unified VM opcodes#63
msinkec merged 9 commits into
masterfrom
agent/update-vm-opcodes

Conversation

@msinkec

@msinkec msinkec commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace fixed-width 64-bit arithmetic and signed conversion handling with unified BigNum arithmetic
  • remove deprecated emulator opcode mappings and emit current equivalents
  • add compiler support for reverseBytes, modExp, ecAdd, ecMul, one-pair ecPairing, sighash, and digest
  • rename new_opcodes.rs to the stable opcode_functions.rs and keep positive opcode emission coverage

Requires the matching VM change

This depends on the introspector pushing OP_INSPECTVERSION, OP_INSPECTLOCKTIME, OP_TXWEIGHT, and OP_INSPECTINPUTSEQUENCE as minimally-encoded BigNums rather than raw 4-byte little-endian arrays. Until that lands, tx.version == 2 compares byte strings and is silently false, and tx.locktime / tx.weight comparisons abort with ErrMinimalData because a raw 4-byte push is non-minimal. Merge after the VM side.

Review fixes

scriptPubKey inspections dropped one stack item too few. OP_INSPECT{IN,OUT}PUTSCRIPTPUBKEY pushes the witness program and the witness version on top of it, so tx.outputs[0].scriptPubKey == x compared x against the version and stranded the program. A shared helper now emits the trailing OP_DROP, used by the indexed input, indexed output, current-input, and this.activeBytecode paths. The <VTXO:...> placeholder resolves to the 32-byte witness program, not a 34-byte serialized P2TR script.

Byte concatenation no longer coerces ints implicitly. bytes + int is now a compile error naming num2bin(value, width). The width and byte order are consensus-visible — they decide what an off-chain signer must hash — so the compiler does not pick one. num2bin writes little-endian sign-magnitude, which matches an unsigned LE64 only for non-negative values, and a value that does not fit the requested width fails the script rather than truncating. The oracle-message contracts in repayment_pool.ark, stability_vault.ark, and stability_offer.ark now state width 8, preserving the bytes they emitted before.

negate() replaced by a unary minus operator. unary_expr takes an optional leading -; Expression::Negate and OP_NEGATE are unchanged. Binary a - b is unaffected. A guard rejects negate(x) with a pointer to -value, since an unknown call otherwise compiles to a silent placeholder.

int is just int. ArkType::Uint32Le is deleted, so tx.version, tx.locktime, tx.weight, and tx.inputs[i].sequence are ordinary ints with no width leaking into contract source. The le64 and le32 witness encodings were unreachable — ArkType::parse maps declared types only and can produce neither — so they are removed from the artifact doc table, arkade-bindgen, and the playground codegen.

digest did not rewrite + to concatenation. Its data operand is parsed as an additive expression, same as sha256, but the concat pass had no arm for it, so digest(a + b, hashType) compiled the concatenation as OP_ADD.

Loop value variables were typed Unknown. The element type of the iterable is now carried into the loop scope, so concatenating a loop value into bytes asks for num2bin like anywhere else instead of passing through implicitly — the same implicit coercion this PR rejects, previously still reachable inside loop bodies.

Loop unrolling dropped the index in most expressions. Only 23 of 46 expression variants were substituted; the rest fell to a catch-all, so a loop value used inside sha256, num2bin, substr, cat, or a concatenation kept the unresolved variable after unrolling. All operand-carrying variants now substitute.

Deferred

ecAdd and ecMul push x and y as two stack items, which the AST cannot represent — let binds one name, and == would only see y. Their operand order is verified against the VM and their emission is correct, but the results cannot be consumed yet. Both carry a TODO(asset-id-struct) tying them to the composite return type that group.assetId already waits on.

Validation

  • cargo test --workspace
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo fmt --check
  • ./playground/build.sh
  • every opcode constant emitted by the compiler exists in the current emulator, and the operand order of OP_ECADD, OP_ECMUL, OP_ECPAIRING (including pair_count before curve_id), OP_MODEXP, OP_DIGEST, and OP_SIGHASH was checked against their handlers

@msinkec
msinkec marked this pull request as ready for review July 27, 2026 14:35
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Playground Preview

A live preview of this PR's playground is available at:
https://arkade-os.github.io/compiler/pr-previews/pr-63/

Built from commit a97aa8a068d5e510a7b797d10e3797499248f238 · Workflow run

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The compiler adds cryptographic and byte-reversal expressions, removes fixed-width numeric conversions, updates type inference and opcode emission, revises introspection handling, removes legacy encodings from bindings, and changes oracle message hashing to explicit num2bin conversions.

Changes

Expression and compiler update

Layer / File(s) Summary
Expression grammar, AST, and typing
src/models/mod.rs, src/parser/*, src/typechecker/mod.rs, src/validator/mod.rs
Adds digest, sighash, modExp, elliptic-curve, unary-negation, and reverseBytes expressions; removes fixed-width conversion variants and Uint64Le inference.
Concatenation rewrite and compiler flow
src/compiler/concat.rs, src/compiler/expr.rs, src/compiler/mod.rs
Requires explicit numeric conversion before byte concatenation, propagates rewrite errors, emits new opcodes, and compiles scriptPubKey values with stack cleanup.
Opcode exports and introspection behavior
src/opcodes/mod.rs, src/compiler/introspection.rs, src/compiler/asset.rs
Adds and removes opcode constants, uses OP_TXID for isFresh, centralizes scriptPubKey emission, and removes issuance and nonce opcode paths.
Encoding removal and generated bindings
arkade-bindgen/*, playground/codegen.js
Removes le64 and le32 encoding variants and their generated Go and TypeScript mappings.
Examples and feature validation
examples/*, tests/features/*, tests/examples/*, playground/arkade-language.js
Updates oracle hashing, covenant opcode expectations, introspection tests, and coverage for new expression opcodes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Source
  participant Parser
  participant Expression
  participant Compiler
  participant ASM
  Source->>Parser: parse new function expression
  Parser->>Expression: construct expression variant
  Expression->>Compiler: compile operands
  Compiler->>ASM: emit opcode sequence
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: tiero, arkanaai

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: aligning the compiler with the unified VM opcode set.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/update-vm-opcodes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/compiler/concat.rs (1)

217-238: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Digest is missing from the concat-rewrite pass — nested + inside digest(...) won't be rewritten to byte concatenation.

Digest.data is parsed via parse_additive_expr (src/parser/crypto.rs::parse_digest) — the exact same path as Sha256.data, which is explicitly handled here (recursing to rewrite nested + into Concat). Digest instead falls through to the other catch-all, which does not recurse into its operands at all. As a result, digest(bytesA + bytesB, hashType) keeps bytesA + bytesB as a raw BinaryOp, and emit_expression_asm will later compile it as arithmetic OP_ADD instead of OP_CAT, producing incorrect bytecode for a bytes-concatenation-then-hash expression.

As per path instructions, Implement language features across the grammar, AST/models, parser, and compiler together; do not add expression or requirement variants without compiler emission and tests — this specific compiler-pass gap (and the missing edge-case test for digest with a + operand) should be closed before merge.

🐛 Proposed fix: add explicit `Digest` arm mirroring `Sha256`
+        Expression::Digest { data, hash_type } => {
+            let (new_data, _) = rewrite_expression_concat(*data, scope);
+            (
+                Expression::Digest {
+                    data: Box::new(new_data),
+                    hash_type,
+                },
+                ArkType::Bytes,
+            )
+        }
         // Leaves and other compound expressions: no `+` to rewrite below the surface.
         other => {
             let t = crate::typechecker::infer_type(&other, scope);
             (other, t)
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/concat.rs` around lines 217 - 238, Update
rewrite_expression_concat to add an explicit Digest arm, recursively applying
rewrite_expression_concat to Digest.data just as the existing Sha256 handling
does, while preserving the other Digest fields and returning the correct
expression type. Add or update the edge-case test covering digest with a
bytes-concatenation operand so nested + is emitted as byte concatenation rather
than arithmetic.

Source: Path instructions

src/parser/grammar.pest (1)

190-232: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

reverseBytes breaks when used as a function-call/constructor argument.

complex_expression's ordered choice was extended with digest_func, sighash_func, negate_func, mod_exp_func, ec_add, ec_mul, ec_pairing — but not reverse_bytes_func (unlike primary_expr, which does include it at Line 152). Since "reverseBytes" is registered in reserved_function_signature (src/parser/expr.rs), any reverseBytes(...) call that must be parsed as a complex_expression (function-call arguments, constructor args, array literals) will instead match the generic function_call alternative and get unconditionally rejected by reject_reserved_function_call with "malformed reserved function call reverseBytes(...)" — even with a syntactically correct single argument. This only works today via require(reverseBytes(...)), which reaches primary_expr through general_expression; no test currently exercises the complex_expression path, so this defect is untested.

🐛 Proposed fix
     digest_func |
     sighash_func |
     negate_func |
     mod_exp_func |
     ec_add |
     ec_mul |
     ec_pairing |
     ec_mul_scalar_verify |
     tweak_verify |
     substr_func |
     cat_func |
     bin2num_func |
     num2bin_func |
+    reverse_bytes_func |
     size_func |

As per coding guidelines, "Implement language features across the grammar, AST/models, parser, and compiler together; do not add expression or requirement variants without compiler emission and tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/grammar.pest` around lines 190 - 232, Update the
complex_expression ordered alternatives to include reverse_bytes_func alongside
the other reserved expression functions, ensuring reverseBytes(...) parses
correctly in function-call, constructor, and array-literal arguments. Reuse the
existing reverse_bytes_func and primary_expr handling; do not alter generic
function-call rejection or add unrelated expression variants.

Source: Coding guidelines

🧹 Nitpick comments (1)
tests/features/opcode_functions.rs (1)

359-361: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing negative-test coverage for removed conversion functions.

test_conversion_chain was deleted, but no test now verifies neg64/le64ToScriptNum/le32ToLe64 produce the new rejection error added in src/parser/expr.rs (reject_reserved_function_call). This is a real behavior change (previously compiled fixed-width ops now hard-error) with zero coverage.

✅ Suggested addition
#[test]
fn test_neg64_is_rejected() {
    let code = r#"
        contract Removed(int a) {
            function check() {
                let x = neg64(a);
                require(true);
            }
        }
    "#;
    let err = compile(code).expect_err("neg64 should be rejected");
    assert!(err.to_string().contains("BigNum arithmetic"), "unexpected error: {err}");
}

As per coding guidelines, "Add integration tests for every new syntax or opcode path, including happy paths and edge conditions."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/features/opcode_functions.rs` around lines 359 - 361, Add negative
integration tests in the opcode function test module covering each removed
conversion function—neg64, le64ToScriptNum, and le32ToLe64—through compilation
attempts that must fail with the new “BigNum arithmetic” rejection error from
reject_reserved_function_call. Replace the deleted test_conversion_chain
coverage with these rejection cases.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/compiler/loops.rs`:
- Around line 276-381: Add an explicit Expression::Negate arm in
substitute_expression that recursively substitutes its value using the same
index_var, value_var, k, and array_name arguments, preserving the Negate
wrapper. Add or update loop-unrolling coverage to verify negate(i) and
negate(arr[i]) resolve to the substituted literal or array element.

---

Outside diff comments:
In `@src/compiler/concat.rs`:
- Around line 217-238: Update rewrite_expression_concat to add an explicit
Digest arm, recursively applying rewrite_expression_concat to Digest.data just
as the existing Sha256 handling does, while preserving the other Digest fields
and returning the correct expression type. Add or update the edge-case test
covering digest with a bytes-concatenation operand so nested + is emitted as
byte concatenation rather than arithmetic.

In `@src/parser/grammar.pest`:
- Around line 190-232: Update the complex_expression ordered alternatives to
include reverse_bytes_func alongside the other reserved expression functions,
ensuring reverseBytes(...) parses correctly in function-call, constructor, and
array-literal arguments. Reuse the existing reverse_bytes_func and primary_expr
handling; do not alter generic function-call rejection or add unrelated
expression variants.

---

Nitpick comments:
In `@tests/features/opcode_functions.rs`:
- Around line 359-361: Add negative integration tests in the opcode function
test module covering each removed conversion function—neg64, le64ToScriptNum,
and le32ToLe64—through compilation attempts that must fail with the new “BigNum
arithmetic” rejection error from reject_reserved_function_call. Replace the
deleted test_conversion_chain coverage with these rejection cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d0116508-0d30-4b9b-bb01-f378ce6764a4

📥 Commits

Reviewing files that changed from the base of the PR and between 02d10ff and 25d083b.

📒 Files selected for processing (23)
  • examples/stability/stability.md
  • src/compiler/asset.rs
  • src/compiler/concat.rs
  • src/compiler/expr.rs
  • src/compiler/introspection.rs
  • src/compiler/loops.rs
  • src/compiler/mod.rs
  • src/models/mod.rs
  • src/opcodes/mod.rs
  • src/parser/crypto.rs
  • src/parser/expr.rs
  • src/parser/grammar.pest
  • src/parser/introspection.rs
  • src/typechecker/mod.rs
  • src/validator/mod.rs
  • tests/examples/arkade_kitties.rs
  • tests/features.rs
  • tests/features/concat_op.rs
  • tests/features/group_properties.rs
  • tests/features/io_introspection.rs
  • tests/features/opcode_functions.rs
  • tests/features/packet_primitives.rs
  • tests/features/type_system.rs
💤 Files with no reviewable changes (1)
  • src/compiler/introspection.rs

Comment thread src/compiler/loops.rs
Fix the scriptPubKey inspections dropping the witness version the opcode
pushes above the program, so `==` compares the program rather than the
version. Applied once in a shared helper and used by the indexed input,
indexed output, current-input, and this.activeBytecode paths.

Reject implicit int coercion in byte concatenation. `bytes + int` now
errors and asks for num2bin(value, width); the compiler no longer picks
a width, since the width and byte order decide what an off-chain signer
must hash. Oracle-message contracts updated to state width 8.

Replace negate() with a unary minus operator and delete Uint32Le, so
version/locktime/sequence/weight are plain ints now that the VM pushes
them as BigNums. Drop the dead le64/le32 encodings from the bindgen and
playground codegen, and substitute the loop index through Negate.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/compiler/concat.rs (1)

17-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two differently-scoped is_numeric helpers with diverging membership.

This module's is_numeric treats Int | Bool as numeric (for deciding when a + operand needs explicit num2bin), while src/typechecker/mod.rs's is_numeric treats only Int as numeric (for comparison-operator compatibility). Same name, same crate, different semantics — a future reader could reasonably assume they're interchangeable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/concat.rs` around lines 17 - 23, Rename the concat-specific
helper is_numeric to a name that clearly reflects its byte-concatenation or
num2bin purpose, and update its call sites in the concatenation logic. Leave the
typechecker::is_numeric helper unchanged, preserving the distinct
Int-versus-Int|Bool semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/compiler/concat.rs`:
- Around line 88-103: Capture the iterable type returned by
rewrite_expression_concat instead of discarding it, then bind value_var in
loop_scope to the array element type when the iterable is ArkType::Array(inner).
Preserve ArkType::Unknown for non-array or unavailable iterable types, while
keeping index_var typed as ArkType::Int and rewriting the loop body through
rewrite_statements_concat.

---

Nitpick comments:
In `@src/compiler/concat.rs`:
- Around line 17-23: Rename the concat-specific helper is_numeric to a name that
clearly reflects its byte-concatenation or num2bin purpose, and update its call
sites in the concatenation logic. Leave the typechecker::is_numeric helper
unchanged, preserving the distinct Int-versus-Int|Bool semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 37733ca9-edb0-4a52-b8f7-4cff1fc136fd

📥 Commits

Reviewing files that changed from the base of the PR and between 25d083b and 6c41fff.

📒 Files selected for processing (25)
  • arkade-bindgen/src/ir.rs
  • arkade-bindgen/src/targets/go.rs
  • arkade-bindgen/src/targets/typescript.rs
  • arkade-bindgen/tests/ir_test.rs
  • examples/bonds/repayment_pool.ark
  • examples/stability/stability.md
  • examples/stability/stability_offer.ark
  • examples/stability/stability_vault.ark
  • playground/arkade-language.js
  • playground/codegen.js
  • src/compiler/concat.rs
  • src/compiler/expr.rs
  • src/compiler/introspection.rs
  • src/compiler/loops.rs
  • src/compiler/mod.rs
  • src/models/mod.rs
  • src/parser/crypto.rs
  • src/parser/expr.rs
  • src/parser/grammar.pest
  • src/typechecker/mod.rs
  • tests/examples/layerzero.rs
  • tests/features/concat_op.rs
  • tests/features/contract_import_instantiation.rs
  • tests/features/opcode_functions.rs
  • tests/features/packet_primitives.rs
💤 Files with no reviewable changes (4)
  • src/parser/crypto.rs
  • arkade-bindgen/src/targets/go.rs
  • arkade-bindgen/src/ir.rs
  • arkade-bindgen/tests/ir_test.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/compiler/loops.rs
  • examples/stability/stability.md
  • src/compiler/introspection.rs
  • tests/features/concat_op.rs
  • src/parser/grammar.pest
  • src/models/mod.rs

Comment thread src/compiler/concat.rs
digest's data operand is parsed as an additive expression like sha256's,
but the concat rewrite skipped it, so `digest(a + b, ht)` compiled the
concatenation as OP_ADD.

Type the loop value variable from the iterable's element type instead of
Unknown, so concatenating it into bytes asks for num2bin like anywhere
else rather than passing through implicitly.

Substitute the loop index through the remaining expression variants that
carry operands. Byte and hashing operations were falling to the catch-all,
so a loop value used inside sha256/num2bin/substr kept the unresolved
variable after unrolling.
@msinkec
msinkec merged commit 12d82c7 into master Jul 28, 2026
5 checks passed
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant