Skip to content

Conversation

@ascandone
Copy link
Contributor

@ascandone ascandone commented Dec 5, 2025

This PR prevents the interpreter to output invalid postings.

We check that, in every posting:

  1. the amount isn't negative!
  2. source and destination name are valid
  3. asset name is valid

Point 1) should be a consequence of the implementation but it's very hard to prove that no such bugs exist. We can't simply disallow negative monetaries to be constructed because they are legit values, although we can't send them around. But they can be involved in expressions like $x - $y + $z (maybe $x-$y is neg, but the whole expr is positive).

The condition 2 and 3 are also checked on runtime, so that invalid assets/account can never exist, at no point of the execution. Still, it's better to double-check at the end of the script.

The regex are taken from the ledger. For the sake of simplicity, we are duplicating this domain data and avoiding depending on Formance common packages. This is justified by the fact that they very rarely change, and we want to control whether we relax the regex on this repo (we don't want to update by mistake to a version of the dependency that relaxes the regex)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 5, 2025

Walkthrough

This change introduces validation for asset and account names with new error types. Monetary and asset parsing now validate names using regex patterns and return custom error types (InternalError, InvalidAsset) on failure. Invariant checks are performed on postings after program computation.

Changes

Cohort / File(s) Change Summary
Error Types
internal/interpreter/interpreter_error.go
Added InternalError struct with Posting field and Error() method; added InvalidAsset struct with Name field and Error() method for reporting validation failures.
Validation Logic
internal/interpreter/interpreter.go
Added checkAccountName(), checkAssetName(), and checkPostingInvariants() functions for name validation. Modified monetary parsing to use NewAsset() for asset conversion. Updated RunProgram() to perform invariant checks on all postings after computation.
Value Objects
internal/interpreter/value.go
Modified NewAccountAddress() to use checkAccountName() instead of validateAddress(). Added NewAsset() function with asset name validation using checkAssetName(). Removed legacy regexp-based validation code.
Tests
internal/interpreter/interpreter_test.go
Added TestBadAssetInMeta test case to validate that invalid asset names referenced through account metadata produce InvalidAsset errors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review new validation functions (checkAccountName, checkAssetName, checkPostingInvariants) and their regex patterns for correctness
  • Verify NewAsset() error handling and integration points in monetary parsing
  • Confirm invariant checks in RunProgram() are appropriately placed and don't suppress valid errors
  • Validate test case coverage for the InvalidAsset error path

Poem

🐰 A hop, skip, and check—
Assets dance with names so true,
Validations bloom anew,
No bad assets slip through the beck! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: prevent invalid outputs' is directly related to the main objective of adding validation checks to prevent invalid postings from being output by the interpreter.
Description check ✅ Passed The description comprehensively explains what invalid outputs are being prevented (negative amounts, invalid account names, invalid asset names) and justifies the implementation approach, directly correlating to the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/prevent-invalid-outputs

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 and usage tips.

@ascandone ascandone force-pushed the feat/prevent-invalid-outputs branch from d784f68 to db5f5c4 Compare December 5, 2025 16:33
@ascandone ascandone marked this pull request as ready for review December 5, 2025 16:35
@ascandone ascandone requested a review from Azorlogh December 5, 2025 16:35
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 4 files

Prompt for AI agents (all 2 issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="internal/interpreter/interpreter.go">

<violation number="1" location="internal/interpreter/interpreter.go:237">
P2: Regex is compiled on every function call. Move `Regexp` to package level for better performance, consistent with existing patterns like `colorRe`, `percentRegex`, and `fractionRegex`.</violation>

<violation number="2" location="internal/interpreter/interpreter.go:244">
P2: Regex is compiled on every function call. Move `Regexp` to package level for better performance, consistent with existing patterns like `colorRe`, `percentRegex`, and `fractionRegex`.</violation>
</file>

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR

// https://github.com/formancehq/ledger/blob/main/pkg/assets/asset.go
func checkAssetName(v string) bool {
const Pattern = `[A-Z][A-Z0-9]{0,16}(_[A-Z]{1,16})?(\/\d{1,6})?`
var Regexp = regexp.MustCompile("^" + Pattern + "$")
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 5, 2025

Choose a reason for hiding this comment

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

P2: Regex is compiled on every function call. Move Regexp to package level for better performance, consistent with existing patterns like colorRe, percentRegex, and fractionRegex.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/interpreter/interpreter.go, line 244:

<comment>Regex is compiled on every function call. Move `Regexp` to package level for better performance, consistent with existing patterns like `colorRe`, `percentRegex`, and `fractionRegex`.</comment>

<file context>
@@ -225,6 +230,40 @@ func (s *programState) parseVars(varDeclrs []parser.VarDeclaration, rawVars map[
+// https://github.com/formancehq/ledger/blob/main/pkg/assets/asset.go
+func checkAssetName(v string) bool {
+	const Pattern = `[A-Z][A-Z0-9]{0,16}(_[A-Z]{1,16})?(\/\d{1,6})?`
+	var Regexp = regexp.MustCompile(&quot;^&quot; + Pattern + &quot;$&quot;)
+	return Regexp.Match([]byte(v))
+}
</file context>
Fix with Cubic

func checkAccountName(addr string) bool {
const SegmentRegex = "[a-zA-Z0-9_-]+"
const Pattern = "^" + SegmentRegex + "(:" + SegmentRegex + ")*$"
var Regexp = regexp.MustCompile(Pattern)
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 5, 2025

Choose a reason for hiding this comment

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

P2: Regex is compiled on every function call. Move Regexp to package level for better performance, consistent with existing patterns like colorRe, percentRegex, and fractionRegex.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/interpreter/interpreter.go, line 237:

<comment>Regex is compiled on every function call. Move `Regexp` to package level for better performance, consistent with existing patterns like `colorRe`, `percentRegex`, and `fractionRegex`.</comment>

<file context>
@@ -225,6 +230,40 @@ func (s *programState) parseVars(varDeclrs []parser.VarDeclaration, rawVars map[
+func checkAccountName(addr string) bool {
+	const SegmentRegex = &quot;[a-zA-Z0-9_-]+&quot;
+	const Pattern = &quot;^&quot; + SegmentRegex + &quot;(:&quot; + SegmentRegex + &quot;)*$&quot;
+	var Regexp = regexp.MustCompile(Pattern)
+	return Regexp.Match([]byte(addr))
+}
</file context>
Fix with Cubic

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
internal/interpreter/interpreter.go (2)

233-247: Centralised name validation is good; consider hoisting regex compilation

The checkAccountName/checkAssetName helpers correctly mirror the ledger regexes, but they recompile their regexes on every call. For modest performance and consistency with colorRe, percentRegex, and fractionRegex, consider hoisting the compiled regexes to package scope and reusing them:

-// https://github.com/formancehq/ledger/blob/main/pkg/accounts/accounts.go
-func checkAccountName(addr string) bool {
-	const SegmentRegex = "[a-zA-Z0-9_-]+"
-	const Pattern = "^" + SegmentRegex + "(:" + SegmentRegex + ")*$"
-	var Regexp = regexp.MustCompile(Pattern)
-	return Regexp.Match([]byte(addr))
-}
+const accountSegmentRegex = "[a-zA-Z0-9_-]+"
+const accountPattern = "^" + accountSegmentRegex + "(:" + accountSegmentRegex + ")*$"
+var accountNameRegexp = regexp.MustCompile(accountPattern)
+
+func checkAccountName(addr string) bool {
+	return accountNameRegexp.MatchString(addr)
+}
 
-// https://github.com/formancehq/ledger/blob/main/pkg/assets/asset.go
-func checkAssetName(v string) bool {
-	const Pattern = `[A-Z][A-Z0-9]{0,16}(_[A-Z]{1,16})?(\/\d{1,6})?`
-	var Regexp = regexp.MustCompile("^" + Pattern + "$")
-	return Regexp.Match([]byte(v))
-}
+const assetPattern = `[A-Z][A-Z0-9]{0,16}(_[A-Z]{1,16})?(\/\d{1,6})?`
+var assetNameRegexp = regexp.MustCompile("^" + assetPattern + "$")
+
+func checkAssetName(v string) bool {
+	return assetNameRegexp.MatchString(v)
+}

252-265: Posting invariants are correct; add nil-amount safety for robustness

The checkPostingInvariants + final loop in RunProgram correctly enforce non‑negative amounts and valid account/asset names before returning postings, which is exactly what the PR is aiming for. One robustness tweak: if a future change ever appends a posting with Amount == nil, posting.Amount.Cmp(...) will panic instead of returning an InternalError. You could guard that case explicitly:

 func checkPostingInvariants(posting Posting) InterpreterError {
-	isAmtNegative := posting.Amount.Cmp(big.NewInt(0)) == -1
+	if posting.Amount == nil {
+		return InternalError{Posting: posting}
+	}
+	isAmtNegative := posting.Amount.Cmp(big.NewInt(0)) == -1
 	// ...
 }

This keeps invariant violations reporting through InternalError instead of panicking, while still treating them as “should never happen” conditions. Based on learnings, this also preserves the existing behaviour where insufficient‑funds errors on colored assets still report the base asset symbol.

Also applies to: 318-323

internal/interpreter/value.go (1)

33-45: Account/asset constructors now enforce validation; clarify or align NewMonetary

Switching NewAccountAddress to checkAccountName and introducing NewAsset gives a clear, centralized validation path and the right InvalidAccountName/InvalidAsset errors for untrusted strings. One small consistency gap is NewMonetary, which still accepts an arbitrary asset string and wraps it as Asset without validation; if this helper is ever used with external input, it could bypass the new checks.

Consider either:

  • documenting that NewMonetary expects an already‑validated asset string, or
  • adding a separate validated constructor (e.g. NewValidatedMonetary(asset string, n int64) (Monetary, InterpreterError)) that uses NewAsset under the hood.

Also applies to: 224-229

internal/interpreter/interpreter_error.go (1)

10-18: New InternalError and InvalidAsset types fit the error model; consider test helper update

InternalError and InvalidAsset are well‑shaped for the new invariant and asset‑validation paths (they embed parser.Range and implement Error() cleanly). To keep tests resilient if you later start populating their Range fields, it may be worth extending removeRange in internal/interpreter/interpreter_test.go to normalize these types as well, similar to MissingFundsErr and TypeError, so struct equality in tests stays focused on semantic fields rather than locations.

Also applies to: 233-240

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 06a7199 and db5f5c4.

📒 Files selected for processing (4)
  • internal/interpreter/interpreter.go (4 hunks)
  • internal/interpreter/interpreter_error.go (2 hunks)
  • internal/interpreter/interpreter_test.go (1 hunks)
  • internal/interpreter/value.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-04-23T16:27:16.351Z
Learnt from: ascandone
Repo: formancehq/numscript PR: 55
File: internal/interpreter/interpreter_test.go:4273-4295
Timestamp: 2025-04-23T16:27:16.351Z
Learning: When a numscript operation fails due to insufficient funds on a colored asset (e.g., "COIN*red"), the error references the uncolored asset (e.g., "COIN") as specified in the script, not the colored version used internally.

Applied to files:

  • internal/interpreter/interpreter_error.go
🧬 Code graph analysis (3)
internal/interpreter/value.go (1)
internal/interpreter/interpreter_error.go (2)
  • InvalidAccountName (224-227)
  • InvalidAsset (233-236)
internal/interpreter/interpreter_test.go (3)
internal/interpreter/interpreter.go (3)
  • AccountsMetadata (27-27)
  • AccountMetadata (26-26)
  • Posting (80-85)
numscript.go (3)
  • AccountsMetadata (63-63)
  • AccountMetadata (60-60)
  • Posting (52-52)
internal/interpreter/interpreter_error.go (1)
  • InvalidAsset (233-236)
internal/interpreter/interpreter_error.go (2)
internal/parser/range.go (1)
  • Range (13-16)
internal/interpreter/interpreter.go (1)
  • Posting (80-85)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Tests
  • GitHub Check: Dirty
🔇 Additional comments (2)
internal/interpreter/interpreter_test.go (1)

225-249: TestBadAssetInMeta correctly asserts InvalidAsset from metadata

This test cleanly exercises the meta() + asset var origin path and confirms that an invalid asset coming from account metadata surfaces as InvalidAsset with the offending name and no postings, matching the new NewAsset validation flow.

internal/interpreter/interpreter.go (1)

95-118: Monetary and asset variables now flow through NewAsset validation

Wiring both parseMonetary and the analysis.TypeAsset branch of parseVar through NewAsset ensures asset strings coming from JSON or meta() are validated and surface as InvalidAsset on failure, which aligns with the runtime validation goals of this PR.

Also applies to: 120-148

@codecov
Copy link

codecov bot commented Dec 5, 2025

Codecov Report

❌ Patch coverage is 72.22222% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.13%. Comparing base (06a7199) to head (db5f5c4).

Files with missing lines Patch % Lines
internal/interpreter/interpreter.go 77.77% 3 Missing and 3 partials ⚠️
internal/interpreter/interpreter_error.go 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #105      +/-   ##
==========================================
- Coverage   68.13%   68.13%   -0.01%     
==========================================
  Files          45       45              
  Lines        4290     4321      +31     
==========================================
+ Hits         2923     2944      +21     
- Misses       1209     1216       +7     
- Partials      158      161       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

2 participants