VBScript Constants Are Not Hoisted

VBScript Constants Are Not Hoisted

  • Comments 10

A reader asked me the other day to riff a bit on why this works:

Dim i
For i = 1 To 2
    print c
Next
Const c = 10

And this works

For i = 1 To 2
    print c
    Dim i
Next
Const c = 10

but this fails with a "name redefined" error:

For i = 1 To 2
    print c
    Const c = 10
    Dim i
Next

To understand what's going on here, there are two preliminaries we should get out of the way. First, refresh your memory on how constants work in VBScript. Second, refresh your memory on the hoisting algorithm.

OK, so we know the following:

  • variable declarations are logically hoisted to the top of the scope
  • constants are evaluated at code compilation time; the constants' values are spit into the code

Now, the weird part. These facts do not imply that constant declarations are hoisted.

Constant declarations appear at first glance as though they are logically hoisted because the code generator spits the values into the code. But there are ways of exposing the fact that they are in fact not hoisted.

In order to illumine how this works, I'll show you guys a bit of the VBScript code generator output. (Unless you've somehow managed to sneak into my office and obtain a debug build of VBScript, you don't have the utility that produces code generator dumps, so don't even ask!)

Let's start simple.

i = 10
print i
Dim i ' will be hoisted

generates this code:

VarBind 'i' 1          Bind name 'i' to local variable slot 1
Bos 0                  beginning of statement 0
IntConst 10            put 10 on stack
LocalSt 1              store top of stack in slot 1
Bos 1
LocalAdr 1             put reference to slot 1 on stack
Call 'print' 1         call print with one argument

(As you can see, the script engine is a "stack machine". Arguments for each opcode are put onto the stack, and each opcode consumes them off the stack. There are no "registers".)

This, on the other hand

print c
Const c = 10

generates this code:

Bos 0
IntConst 10
Call 'print' 1
Bos 1
IntConst 10
ConstSt 'c'

Two things pop out. First, the call to print is not referencing any kind of slot; it just gets the number 10. Second, no declaration for 'c' is hoisted to the top. The slot 'c' is not bound until the statement runs; it appears to be bound earlier, but that's an illusion created by the code generator spitting the constant.

Now it should be clear what happens here:

For i = 1 To 2
    print c
    Const c = 10
Dim i
Next

This generates the code

VarBind 'i' 1
Bos 0
IntConst 1           from
IntConst 2           to
IntConst 1           step
For 1                use slot 1 for the loop variable
Bos 1
IntConst 10
Call 'print' 1
Bos 2
IntConst 10
ConstSt 'c'
Bos1 3
Next

The first time that the constant is stored, it creates a new named slot called 'c', assigns the value, and marks the slot as "not writable". The second time, it tries to create the new slot, discovers that it exists, and raises an error.

OK, this is a little weird, but it seems to be behaving pretty sensibly. Though the declaration is not hoisted, the code spit lets you use the value before the slot is created. Though the slot is created later, it can only be stored once, as you'd expect with a constant.

What gets really weird is when you make use of the slot before it is bound:

c = 11
print c
Const c = 10

This prints out "10" and then dies on the third line. The first line creates a named slot. The second line prints out the compile-time spit constant. The third line attempts to create a read-only slot but discovers that a slot with that name already exists, and errors out.

Bos 0
IntConst 11
NamedSt 'c'
Bos 1
IntConst 10
Call 'print' 1
Bos 2
IntConst 10
ConstSt 'c'

For similar reasons, this doesn't do what you might think:

print TypeName(c)
print Eval("TypeName(c)")
Const c = 10

The first statement prints "Integer", because 10 is spit into the code gen, not a reference to 'c'. But a brand-new code generator is created when Eval is called, and it has no idea that earlier this value was bound to 10 at compile time. The evaluation engine therefore creates a new slot called 'c', initializes it to Empty, and runs the code, which prints "Empty". Then, when the third statement runs, it fails because the slot already exists. Ouch! This is one of the weird situations where evaluating an expression with Eval yields a different result than just evaluating the expression normally.

Long story short: as Groucho Marx once said, if it hurts when you do that then don't do that. Put your constant declarations at the top of your program like a sensible person and no one gets hurt!

  • So in these statements,

    print TypeName(c)
    print Eval("TypeName(c)")
    Const c = 10

    So on which line does the slot 'c' get created? on the first or the second? or is it both lines since they are in a different context?
  • I think we should go back to line numbers. At least then you KNOW what order things are going to happen in!

    (Unless you use FORTRAN, in which case line numbers make no difference to execution order.)
  • The second. As you can see from the code gen, the first line is exactly equivalent to 'print typename(10)'.
  • From a design standpoint, was there an advantage to not hoisting the allocation of a slot for a constant to the top of scope? Or did noone notice that it was a problem until it was too late to go and change it?

    By the way, thanks for the conversion help the other day. By bad design indeed, unfortunately.
  • I'm pretty sure that this was a "no one noticed it" issue. But that particular design decision predates my time here, so I can't be sure.
  • Shouldn't "Call Print 1" be "Call Print 10" instead? Otherwise, something is REALLY wrong.

    This, on the other hand

    print c
    Const c = 10

    generates this code:

    Bos 0
    IntConst 10
    Call 'print' 1
    Bos 1
    IntConst 10
    ConstSt 'c'
  • > Shouldn't "Call Print 1" be "Call Print 10"

    No. Read my comments above again:

    LocalAdr 1 put reference to slot 1 on stack
    Call 'print' 1 call print with one argument

    So "IntConst 10" puts 10 on the argument stack, "Call print 1" calls print and passes one argument from the argument stack.

    If it had been "print 10, 20" then it would have been

    IntConst 10
    IntConst 20
    Call 'print' 2

    Make sense?


  • I want to do this:

    dim strValue
    strValue = "Hello World"
    const MY_CONST = "Hi" & strValue

    But it doesn't work. Is there a way around it? I need to take the string value of the variable and put it into the constant.
  • Read the link "how constants work in VBScript" above for the answer to your question.
  • Thanks so much for posting this info on how constants are evaluated, it's been very very useful.  Given that constants are not hoisted and they are handled using very limited constant folding, is there any possibility that same named constants could unintentionally get their values crossed?  For example:

    Given the following .asp files

    common/common.asp:

    --------------

    Sub printConstValue

      response.write "Const Value: " & MY_CONST

    End Sub

    app_A/page_A.asp:

    -----------------

    <!--#INCLUDE FILE="../common/common.asp" -->

    const MY_CONST = "A"

    app_A/page_A1.asp:

    -----------------

    <!--#INCLUDE FILE="page_A.asp" -->

    call printConstValue

    app_B/page_B.asp:

    -----------------

    <!--#INCLUDE FILE="../common/common.asp" -->

    const MY_CONST = "B"

    app_B/page_B1.asp:

    -----------------

    <!--#INCLUDE FILE="page_B.asp" -->

    call printConstValue

    Sometimes calling printconstValue from page B will print out const value declared in file A?  I have situation similar to this and I've been scratching my head trying to figure out why every now and then the values of the constants are getting "crossed".  

Page 1 of 1 (10 items)