Skip to content

fix: load negative integer literals as numbers (#307) - #311

Merged
kkozik-amplify merged 3 commits into
amplify-education:mainfrom
livingstaccato:fix/negative-int-literals
Aug 24, 2026
Merged

fix: load negative integer literals as numbers (#307)#311
kkozik-amplify merged 3 commits into
amplify-education:mainfrom
livingstaccato:fix/negative-int-literals

Conversation

@livingstaccato

Copy link
Copy Markdown
Contributor

Fixes #307.

x = -3 serialized to the expression string ${-3} rather than the int -3, while x = -3.5 still produced a float. Code reading numbers out of a parsed configuration got a string whenever the value happened to be integral.

Verified in clean per-version environments (all on lark 1.3.1): correct in 7.2.1 and 7.3.1, wrong in 8.0.0rc1, 8.1.0 and 8.1.2 — so it arrived with the v8 rewrite, and it undoes the grammar fix from #182.

Why not fix the lexer

INT_LITERAL already permits a leading minus, so the tempting fix is to let it win. That breaks subtraction: MINUS is also the binary operator, and 10 -3 has to keep parsing as 10 - 3 — which it does in both 7.2.1 and 8.1.2 today. FLOAT_LITERAL escapes the ambiguity only because its pattern cannot be confused with an operator followed by a digit.

So the fix goes where the parse has already settled the question. In UnaryOpRule.serialize, when a unary - applies to something that serialized to a number and the operation is the whole value, emit the negated number.

Everything else keeps the ${...} form: -var.count has no literal value, !flag is not arithmetic, 1 + -3 is a larger expression whose operand must stay concatenable text, -1e10 serializes to a string under preserve_scientific_notation, and a force_operation_parentheses result cannot carry its parentheses as a bare number.

Tests

  • integers round-trip suite mirroring the existing floats one, exercising negative literals bare, in tuples and in objects, alongside subtraction and negated references.
  • Unit tests on UnaryOpRule and API-level tests covering both halves of the trade-off.

Without the source change, 12 of the new tests fail. nose2 --config tox.ini: 1410 tests, OK. ruff check and ruff format --check clean. No change to default behaviour beyond the reported bug.


This pull request, and the investigation behind it, were produced by an AI assistant (Claude) working on behalf of the author. Every reproduction, test run and benchmark cited was executed rather than inferred, but please review with that provenance in mind.

`x = -3` serialized to the expression string `${-3}` rather than the int
`-3`, while `x = -3.5` still produced a float. Downstream code reading
numbers out of a parsed configuration got a string whenever the value
happened to be integral, and the 7.x behaviour (fixed in amplify-education#182) was lost.

MINUS is both the unary sign and the binary subtraction operator, so the
sign cannot simply be folded into INT_LITERAL: `10 -3` has to keep
parsing as a subtraction. FLOAT_LITERAL escapes this only because its
pattern cannot be confused with an operator followed by a digit.

Recombine the two at serialization instead, where the parse has already
settled the question: when a unary `-` is applied to something that
serialized to a number, and the operation is the whole value, emit the
negated number. Everything else keeps the `${...}` form -- `-var.count`
has no literal value, `!flag` is not arithmetic, `1 + -3` is a larger
expression whose operand must stay concatenable text, and a
`force_operation_parentheses` result cannot carry its parentheses as a
bare number.

Add an `integers` round-trip suite mirroring the existing `floats` one,
plus unit tests covering both halves of the trade-off. Without the fix,
12 of the new tests fail.
@livingstaccato
livingstaccato requested a review from a team as a code owner August 18, 2026 18:29
The bool-operand test asserted "${-True}", which no HCL input can
produce: UnaryOpRule serializes its operand with inside_dollar_string
set, and LiteralValueRule yields the string "true" in that context.
Assert the helper contract directly instead, and add TestNegatedKeywords
for the path a parse actually takes.

-1e10 lexes as a single FLOAT_LITERAL and never reaches the unary path.
Add the spaced form, which does, and whose operand is a string under
preserve_scientific_notation.

Pin -0 normalization, and add -0 and -true to the integers round-trip
suite. The spaced forms stay out of that fixture because the direct
pipeline normalizes the space away, which would break the byte-exact
direct-reconstruct assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
amplify-education#312 landed the empty-heredoc fix, which touched the same two append-only
spots as this branch. Both conflicts keep both sides:

  CHANGELOG.md      - both entries, amplify-education#309's landed one first so this
                      branch's diff is a pure append
  test_api.py       - TestEmptyHeredocs from main, then this branch's
                      TestNegativeIntegerLiterals and TestNegatedKeywords

No source overlap: this branch only touches hcl2/rules/expressions.py,
amplify-education#312 only touched hcl2/hcl2.lark.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kkozik-amplify
kkozik-amplify merged commit 205f4ba into amplify-education:main Aug 24, 2026
kkozik-amplify added a commit to livingstaccato/python-hcl2 that referenced this pull request Aug 24, 2026
amplify-education#312 and amplify-education#311 have landed since this branch was opened. Both conflicts
are append-only and keep both sides:

  CHANGELOG.md      - landed entries first, so this branch's two lines
                      are a pure append
  test_api.py       - TestEmptyHeredocs, TestNegativeIntegerLiterals and
                      TestNegatedKeywords from main, then this branch's
                      TestStripStringQuotes

No source overlap: this branch touches hcl2/rules/strings.py and
hcl2/utils.py, which neither amplify-education#311 (expressions.py) nor amplify-education#312 (hcl2.lark)
went near.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kkozik-amplify added a commit to agu2347/python-hcl2 that referenced this pull request Aug 24, 2026
amplify-education#312, amplify-education#311 and amplify-education#313 have landed. Two conflicts:

  hcl2/hcl2.lark  - real overlap with amplify-education#312, both editing the heredoc
                    terminals. Resolved as the union: amplify-education#312's lazy optional
                    body group plus this branch's \r?, giving
                    /<<MARKER\r?\n(?:(?:.|\n)*?\r?\n)??\s*MARKER\r?\n/.
                    Verified an empty heredoc in a CRLF file parses, two
                    consecutive empty CRLF heredocs stay separate, and a
                    line merely ending in the marker still does not
                    terminate the body.
  CHANGELOG.md    - landed entries first, this branch's appended.

hcl2/rules/strings.py and hcl2/utils.py auto-merged against amplify-education#313, which
touched both. Checked rather than assumed: strip_string_quotes on CRLF
source, escapes resolved through CRLF line endings, a literal CR inside a
string surviving, and amplify-education#313's out-of-range and lone-surrogate escape
guards all still behave.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kkozik-amplify added a commit to agu2347/python-hcl2 that referenced this pull request Aug 24, 2026
amplify-education#312, amplify-education#311 and amplify-education#313 have landed. Only CHANGELOG.md conflicted, resolved
with the landed entries first and this branch's appended.

hcl2/rules/strings.py auto-merged against amplify-education#313, which rewrote the same
file's string-serialization path. Verified the two coexist: amplify-education#313's
process_escape_sequences and lark_name() dispatch sit alongside this
branch's _strip_closing_marker_line, and the full suite passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Regression in 8.x: negative integer literals load as ${-N} expression strings (regression of #102)

2 participants