Skip to content

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

Description

@livingstaccato

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingregressionSomething broke in a new versionserializationLarkElement IR to Python dict (forward path)unreleasedFix merged into main; waiting for version releaseworkaround-availableIssue can be solved by a workaround

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions