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

Adds account and asset name validation, new error types (InternalError, InvalidAsset), uses NewAsset during parsing, enforces posting invariants after RunProgram, and adds a test for invalid asset metadata.

Changes

Cohort / File(s) Change Summary
Error types
internal/interpreter/interpreter_error.go
Added InternalError (contains parser.Range and Posting) and InvalidAsset (contains parser.Range and Name) with Error() methods to represent invariant/validation failures.
Interpreter logic & invariants
internal/interpreter/interpreter.go
Introduced accountSegmentRegex/accountNameRegex, assetNameRegexp, checkAccountName, checkAssetName, and checkPostingInvariants. Monetary parsing now uses NewAsset(...). RunProgram() validates all postings and returns InternalError on violation.
Value objects / validation
internal/interpreter/value.go
Added NewAsset(src string) (Asset, InterpreterError) that uses checkAssetName; NewAccountAddress now uses checkAccountName. Removed legacy regexp-based validation variables/imports.
Tests
internal/interpreter/interpreter_test.go
Added TestBadAssetInMeta asserting that an invalid asset name referenced via account metadata produces an InvalidAsset error.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through rules and names tonight,
Checked every asset, made sure they’re right.
I guarded postings, one by one,
Till ledger rules were safely done.
A carrot cheer for tidy light! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: prevent invalid outputs' directly aligns with the main objective of the PR, which adds validation to prevent invalid postings from being output by the interpreter.
Description check ✅ Passed The description clearly explains the purpose of preventing invalid outputs, details the three validation checks implemented, and justifies the design decisions around negative monetaries and regex duplication.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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

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 67.74194% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.84%. Comparing base (5bae124) to head (768ab11).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/interpreter/interpreter.go 72.72% 3 Missing and 3 partials ⚠️
internal/interpreter/interpreter_error.go 0.00% 4 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #105   +/-   ##
=======================================
  Coverage   67.83%   67.84%           
=======================================
  Files          45       45           
  Lines        4396     4422   +26     
=======================================
+ Hits         2982     3000   +18     
- Misses       1254     1260    +6     
- Partials      160      162    +2     

☔ 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.

@ascandone ascandone force-pushed the feat/prevent-invalid-outputs branch from f18da4a to 56fcc0d Compare January 8, 2026 10:17
@ascandone ascandone enabled auto-merge (squash) January 8, 2026 10:18
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 (1)
internal/interpreter/interpreter.go (1)

249-266: Consider including Range information in InternalError for better debugging.

The checkPostingInvariants function correctly validates all three invariants, but the returned InternalError doesn't populate the Range field. While the Posting provides the account and asset names, having location information would help users identify which statement in their script produced the invalid posting.

If range information is available at the call site or can be tracked during posting creation, consider passing it to improve error messages.

📜 Review details

Configuration used: Organization 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 db5f5c4 and 56fcc0d.

📒 Files selected for processing (1)
  • internal/interpreter/interpreter.go
🧰 Additional context used
🧬 Code graph analysis (1)
internal/interpreter/interpreter.go (2)
internal/interpreter/value.go (5)
  • NewAsset (40-45)
  • Monetary (21-24)
  • Monetary (29-29)
  • Asset (17-17)
  • Asset (31-31)
internal/interpreter/interpreter_error.go (1)
  • InternalError (10-13)
🔇 Additional comments (4)
internal/interpreter/interpreter.go (4)

108-117: LGTM! Early asset validation prevents invalid assets.

The change to use NewAsset ensures asset names are validated during parsing, catching invalid assets before they can propagate through the system.


135-135: LGTM! Consistent asset validation.

Using NewAsset for asset variable parsing ensures all asset values are validated regardless of how they enter the system.


319-324: LGTM! Final safeguard ensures no invalid postings are produced.

This validation loop correctly implements the PR's objective to prevent invalid outputs. It runs after all program logic completes but before returning results, ensuring:

  • No negative amounts in final postings (while allowing negative intermediates during computation)
  • All account and asset names are valid
  • The system fails fast on the first invalid posting

233-247: The regexes are correct and functional. Verification confirms that colored assets (using the _[A-Z]{1,16} pattern) are properly supported by the assetNameRegexp. Both regex patterns work as intended:

  • Account regex correctly validates alphanumeric, underscore, and hyphen characters with colon-separated segments
  • Asset regex correctly validates uppercase-starting assets with optional colored suffix and ledger notation (/digits)

No changes needed. The implementation is sound.

@ascandone ascandone force-pushed the feat/prevent-invalid-outputs branch from 56fcc0d to 768ab11 Compare January 8, 2026 12:06
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 (2)
internal/interpreter/interpreter_test.go (1)

59-76: Consider adding InvalidAsset to removeRange for consistency.

The removeRange helper normalizes error ranges for test comparisons but doesn't handle the new InvalidAsset error type. While the current test passes (both expected and actual have zero-valued ranges), adding it would ensure consistency with other error types and prevent future test fragility.

♻️ Suggested addition
 func removeRange(e machine.InterpreterError) machine.InterpreterError {
 	switch e := e.(type) {
 	case machine.MissingFundsErr:
 		e.Range = parser.Range{}
 		return e
 	case machine.TypeError:
 		e.Range = parser.Range{}
 		return e
 	case machine.InvalidTypeErr:
 		e.Range = parser.Range{}
 		return e
 	case machine.NegativeAmountErr:
 		e.Range = parser.Range{}
 		return e
+	case machine.InvalidAsset:
+		e.Range = parser.Range{}
+		return e
+	case machine.InternalError:
+		e.Range = parser.Range{}
+		return e
 	default:
 		return e
 	}
 }
internal/interpreter/interpreter.go (1)

249-266: Consider providing more specific error details for debugging.

The function correctly validates all posting invariants, but when a violation occurs, InternalError doesn't indicate which invariant failed (negative amount, invalid asset, invalid source, or invalid destination). This could make debugging harder in production.

♻️ Optional: Add specific failure reason
-// Check the following invariants:
-//   - no negative postings
-//   - no invalid account names
-//   - no invalid asset names
-func checkPostingInvariants(posting Posting) InterpreterError {
-	isAmtNegative := posting.Amount.Cmp(big.NewInt(0)) == -1
-
-	isInvalidPosting := (isAmtNegative ||
-		!checkAssetName(posting.Asset) ||
-		!checkAccountName(posting.Source) ||
-		!checkAccountName(posting.Destination))
-
-	if isInvalidPosting {
-		return InternalError{Posting: posting}
-	}
-
-	return nil
-}
+// Check the following invariants:
+//   - no negative postings
+//   - no invalid account names
+//   - no invalid asset names
+func checkPostingInvariants(posting Posting) InterpreterError {
+	if posting.Amount.Cmp(big.NewInt(0)) == -1 {
+		return InternalError{Posting: posting, Reason: "negative amount"}
+	}
+	if !checkAssetName(posting.Asset) {
+		return InternalError{Posting: posting, Reason: "invalid asset name"}
+	}
+	if !checkAccountName(posting.Source) {
+		return InternalError{Posting: posting, Reason: "invalid source account"}
+	}
+	if !checkAccountName(posting.Destination) {
+		return InternalError{Posting: posting, Reason: "invalid destination account"}
+	}
+	return nil
+}

This would require adding a Reason string field to InternalError.

📜 Review details

Configuration used: Organization 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 56fcc0d and 768ab11.

📒 Files selected for processing (4)
  • internal/interpreter/interpreter.go
  • internal/interpreter/interpreter_error.go
  • internal/interpreter/interpreter_test.go
  • internal/interpreter/value.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-12-05T11:42:58.472Z
Learnt from: ascandone
Repo: formancehq/numscript PR: 27
File: internal/interpreter/interpreter.go:667-668
Timestamp: 2024-12-05T11:42:58.472Z
Learning: In Go test files within the `internal/interpreter` package (e.g., `reconciler_test.go` and `interpreter_test.go`), it's acceptable to use hardcoded `"<kept>"` strings in test data and comments, and they do not need to be replaced with the `KEPT_ADDR` constant.

Applied to files:

  • internal/interpreter/interpreter_test.go
📚 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 (4)
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)
internal/interpreter/interpreter.go (3)
internal/interpreter/value.go (5)
  • NewAsset (40-45)
  • Monetary (21-24)
  • Monetary (29-29)
  • Asset (17-17)
  • Asset (31-31)
numscript.go (2)
  • Posting (52-52)
  • InterpreterError (74-74)
internal/interpreter/interpreter_error.go (1)
  • InternalError (10-13)
internal/interpreter/value.go (3)
internal/interpreter/interpreter_error.go (2)
  • InvalidAccountName (224-227)
  • InvalidAsset (233-236)
internal/interpreter/interpreter.go (1)
  • InterpreterError (73-76)
numscript.go (1)
  • InterpreterError (74-74)
🔇 Additional comments (8)
internal/interpreter/interpreter_test.go (1)

225-249: LGTM! Test correctly validates asset name validation via metadata.

The test properly verifies that an invalid asset name ("Aa" - lowercase letters not allowed) resolved through account metadata triggers an InvalidAsset error.

internal/interpreter/value.go (1)

33-45: LGTM! Clean implementation of validation constructors.

Both NewAccountAddress and NewAsset follow a consistent pattern: validate input using the respective check function, return an appropriate error on failure, or return the typed value on success. This ensures invalid values cannot be constructed.

internal/interpreter/interpreter.go (4)

108-117: LGTM! Asset validation added to monetary parsing.

The change correctly validates the asset name before constructing the Monetary value, ensuring invalid assets cannot enter the system through monetary literals.


134-136: LGTM! Asset variable parsing now validates input.

Asset type variables from JSON input are now validated through NewAsset, consistent with how account variables are validated via NewAccountAddress.


233-247: LGTM! Validation regex patterns are precompiled and well-documented.

The regex patterns are correctly precompiled at package initialization for performance. The comments referencing the ledger source files provide good traceability for the duplicated patterns.


319-324: LGTM! Invariant checks correctly positioned as final validation.

The post-execution validation loop serves as a safety net, ensuring no invalid postings escape even if intermediate expressions involve negative values. This aligns with the PR's design rationale of allowing intermediate negative monetaries while enforcing non-negativity only at output.

internal/interpreter/interpreter_error.go (2)

10-17: LGTM! InternalError type correctly captures invariant violations.

The error type appropriately embeds parser.Range to satisfy the InterpreterError interface and includes the full Posting for debugging context.


233-240: LGTM! InvalidAsset error type follows established patterns.

The structure mirrors InvalidAccountName with the embedded parser.Range and Name field, providing a consistent error interface and clear error message.

@ascandone ascandone merged commit 9caec53 into main Jan 8, 2026
6 of 7 checks passed
@ascandone ascandone deleted the feat/prevent-invalid-outputs branch January 8, 2026 13:10
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.

3 participants