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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions