Describe the bug
Negative integer literals are no longer loaded as numbers. x = -3 serializes to the expression string '${-3}' instead of the int -3. Negative floats are unaffected (-3.5 still loads as -3.5), so the two diverge.
This is a regression of the grammar fix in #182 ("fix e-notation and negative numbers literals", released in 6.1.0), which touched hcl2/hcl2.lark; the v8 grammar rewrite appears to have lost it. The original report of the behaviour was #102.
Software:
-
OS: macOS 15 (arm64)
-
Python version: 3.11.16
-
python-hcl2 versions tested, each in a clean isolated environment with lark 1.3.1:
- 7.2.1 — correct
- 7.3.1 — correct
- 8.0.0rc1 — wrong
- 8.1.0 — wrong
- 8.1.2 — wrong
(8.1.1 could not be installed while testing — PyPI returned 502 for its metadata — but it sits between two affected releases.)
So this arrived with the v8 rewrite rather than in a later 8.1.x fix.
Snippet of HCL2 code causing the unexpected behaviour:
neg_int = -3
neg_float = -3.5
in_tuple = [-1, 2]
in_object = { a = -1, b = 2 }
Expected behavior
The 7.x result, where negative integers are ints:
{
'neg_int': -3,
'neg_float': -3.5,
'in_tuple': [-1, 2],
'in_object': {'a': -1, 'b': 2},
}
Actual behavior (8.1.2)
{
'neg_int': '${-3}',
'neg_float': -3.5,
'in_tuple': ['${-1}', 2],
'in_object': {'a': '${-1}', 'b': 2},
}
Reproduction:
import hcl2
print(hcl2.loads("neg_int = -3\nneg_float = -3.5\nin_tuple = [-1, 2]\n"))
Neither strip_string_quotes=True nor explicit_blocks=False changes it — the value is a bare interpolation, not a quoted string.
Root cause
-3 no longer lexes as a single INT_LITERAL; it parses as a unary operation, which serializes through the expression path and comes back wrapped in ${...}:
>>> import hcl2; print(hcl2.parses_to_tree("x = -3\n").pretty())
start
body
attribute
identifier x
=
unary_op
-
expr_term
int_lit 3
new_line_or_comment
-3.5 still matches FLOAT_LITERAL directly, which is why floats escaped the change.
Impact
Downstream code that reads numbers out of a parsed configuration silently gets a string instead, and the type of a value now depends on whether it happens to be integral. Consumers converting into a typed system (we map into a Terraform-style type system) either raise a type error or silently accept '${-3}' as a string.
No entry in CHANGELOG.md for any 8.x release mentions it, and docs/06_migrating_to_v8.md does not either — the most recent changelog mention of negative-number literals is the 6.1.0 fix above, so this reads as unintentional rather than a documented behaviour change.
Workaround, for anyone else hitting this
With default options the two cases are distinguishable, so post-processing is safe: a bare negative literal arrives as '${-3}' while a genuine template arrives with its quotes intact as '"${-3}"'. Converting only strings that fully match ^\$\{-\d+\}$ therefore recovers the number without touching templates.
That distinction is lost under strip_string_quotes=True, where both collapse to '${-3}' — so the workaround and that option are mutually exclusive.
Describe the bug
Negative integer literals are no longer loaded as numbers.
x = -3serializes to the expression string'${-3}'instead of the int-3. Negative floats are unaffected (-3.5still loads as-3.5), so the two diverge.This is a regression of the grammar fix in #182 ("fix e-notation and negative numbers literals", released in 6.1.0), which touched
hcl2/hcl2.lark; the v8 grammar rewrite appears to have lost it. The original report of the behaviour was #102.Software:
OS: macOS 15 (arm64)
Python version: 3.11.16
python-hcl2 versions tested, each in a clean isolated environment with lark 1.3.1:
(8.1.1 could not be installed while testing — PyPI returned 502 for its metadata — but it sits between two affected releases.)
So this arrived with the v8 rewrite rather than in a later 8.1.x fix.
Snippet of HCL2 code causing the unexpected behaviour:
Expected behavior
The 7.x result, where negative integers are ints:
{ 'neg_int': -3, 'neg_float': -3.5, 'in_tuple': [-1, 2], 'in_object': {'a': -1, 'b': 2}, }Actual behavior (8.1.2)
{ 'neg_int': '${-3}', 'neg_float': -3.5, 'in_tuple': ['${-1}', 2], 'in_object': {'a': '${-1}', 'b': 2}, }Reproduction:
Neither
strip_string_quotes=Truenorexplicit_blocks=Falsechanges it — the value is a bare interpolation, not a quoted string.Root cause
-3no longer lexes as a singleINT_LITERAL; it parses as a unary operation, which serializes through the expression path and comes back wrapped in${...}:-3.5still matchesFLOAT_LITERALdirectly, which is why floats escaped the change.Impact
Downstream code that reads numbers out of a parsed configuration silently gets a string instead, and the type of a value now depends on whether it happens to be integral. Consumers converting into a typed system (we map into a Terraform-style type system) either raise a type error or silently accept
'${-3}'as a string.No entry in
CHANGELOG.mdfor any 8.x release mentions it, anddocs/06_migrating_to_v8.mddoes not either — the most recent changelog mention of negative-number literals is the 6.1.0 fix above, so this reads as unintentional rather than a documented behaviour change.Workaround, for anyone else hitting this
With default options the two cases are distinguishable, so post-processing is safe: a bare negative literal arrives as
'${-3}'while a genuine template arrives with its quotes intact as'"${-3}"'. Converting only strings that fully match^\$\{-\d+\}$therefore recovers the number without touching templates.That distinction is lost under
strip_string_quotes=True, where both collapse to'${-3}'— so the workaround and that option are mutually exclusive.