tech

[English] [Japanese]

07. Variables

This time, I will explain about LuneScript variables.

variable

LuneScript is a statically typed language and variables have types.

Variables are declared with let like this:

// @lnsFront: ok
let val:int = 1;

The above example declares a variable val with an int of 1 as its initial value.

After the variable name, specify the type.

Note that if the initial value is set to real 1.0 instead of int 1, a compilation error will occur because the types are different.

// @lnsFront: error
let val:int = 1.0;  // error

Also, variable declarations now require an initial value.

This is to prevent access to uninitialized variables.++In the future, let flow analysis determine if a variable holds a value,++I'm thinking of making the initial value unnecessary.

Initialization when declaring variables is not required. A compile error will occur if a variable that has not been initialized is referenced. More on that later.

Also, type inference is possible even if the variable is not initialized when declared.

type inference

LuneScript supports type inference.

You can determine the type of a variable from the initial value you set to it. This allows you to declare variables without specifying their type, like this:

// @lnsFront: ok
let val1 = 1; // int 
let val2 = 1.0; // real
let val3 = "abc"; // str

In this case, val1 is treated as int, val2 as real, and val3 as str.

Examples of when you need to specify the type are:

  • Set the initial value of a nilable type variable to nil
let mut val:int! = nil;
  • Set an immediate empty value ([], {}, etc.) to the initial value of a list type or map type variable
let mut val:List<int> = [];
  • When setting a subclass instance to a class type variable such as the following, if you want the variable type to be the superclass type
let val:Super = new Sub();

Initializing variables

Referencing an uninitialized variable will result in a compilation error.

// @lnsFront: error
{
   let val;
   print( "%s" ( val ) ); // error
}

In print() above, an uninitialized val is accessed, which causes a compilation error.

flow analysis

Variable Initialization parses the flow and checks for paths with variable uninitialization.

For example, the following will result in an error:

// @lnsFront: error
fn func( flag:bool )
{
   let val;
   if flag {
     val = 1;
   }
   print( val ); // error
}

The reason for the above error is that val is initialized when flag is true, but val is not initialized when it is false.

All paths need to be initialized before access, like this:

// @lnsFront: ok
fn func( flag:bool )
{
   let val;
   if flag {
     val = 1;
   }
   else {
     val = 2;
   }
   print( val ); // ok
}

Note that this process is an initialization for the variable val, not a rewrite for val, so there is no need for the mut declaration described later.

By the way, even if it is a little complicated like the following, we will analyze the flow.

// @lnsFront: error
fn func( kind:int )
{
   let val;
   if kind < 10 {
      if kind > 0 {
         val = 1;
      }
      else {
         if kind == 0 {
            val = 2;
         }
         elseif kind == 1 {
            val = 3;
         }
         // ※ 
      }
   }
   else {
      val = 4;
   }
   print( val ); // error
}

I think it's a little hard to understand, but at the position of * above, the val reference in print is an error because the initialization of val is missing when else.

Note that an error will also occur at print( val ) in the following cases.

// @lnsFront: error
fn func( flag:bool )
{
   let val;
   fn sub() {
      print( val ); // error
   }
   val = 1;
   sub();
}

Normally val is initialized when running sub(), so it shouldn't be an error, but this is the current design.

type inference

Type inference is possible even if the variable is not initialized when declared.

However, type inference uses the first type assigned in flow analysis.

For example, if

// @lnsFront: error
fn func( flag:bool )
{
   let val;
   if flag {
      val = 1;
   }
   else {
      val = 1.0; // error
   }
}

At the first val = 1 val is of type int. Then, at the next val = 1.0, an error occurs because it is trying to assign 1.0 of real to int type val.

In the above case, the error can be avoided by declaring the type when declaring the variable as follows.

// @lnsFront: ok
fn func( flag:bool )
{
   let val:stem; // stem 型を宣言
   if flag {
      val = 1;
   }
   else {
      val = "a";
   }
   print( val );
}

There are other cases besides stem where types must be declared. For example, there are cases where you want to use a variable of the superclass type, or you want to use a nilable type variable.

shadowing

LuneScript prohibits variable declarations with the same name.

The same name here refers not only to the same name within the same scope, but also to the same name within the accessible scope.

Specifically, the following variable declaration will result in an error.

// @lnsFront: error
{
   let val = 1;
   {
      let val = 1;  // error
   }
}

I think there are pros and cons to this specification, but I'm sticking to the safe side and making it this specification.

access control

Declared variables are treated as local variables.

If you want to publish it to an external module, declare it with pub appended like this:

// @lnsFront: ok
pub let val = 1;

If you want to access variables exposed externally, use import like this:

// @lnsFront: skip
import SubModule;
print( SubModule.val );

where SubModule is the LuneScript module (SubModule.lns) that declares pub let val = 1;.

If you want to access this val, you can access it with SubModule.val.

pub is the basic way to expose variables to external modules, but you can also use global .

// @lnsFront: ok
pub let val1 = 1;
global let val2 = 2;

The difference between pub and global is the namespace difference.

It's easy to understand if you look at the following example, but this is a sample that accesses the above val1 and val2 from the outside.

// @lnsFront: skip
import SubModule;
print( SubModule.val1 );
print( val2 );

val1 is accessed as a variable in SubModule's namespace as SubModule.val1 , but val2 is accessed as a variable in the top-level namespace.

When developing a system with only LuneScript, I don't think you should use global (or rather, you should avoid using global ), but when processing in conjunction with other Lua modules, use global I think there are times when you have to.

For that compatibility, we support global .

There are the following global constraints.

"Variables declared as global become effective when the module that declares them is imported."

For example, in the following example val2 seems to exist in SubModule without any relation,

// @lnsFront: skip
import SubModule;
print( SubModule.val1 );
print( val2 );

In the following case, an error occurs because val2 does not exist because SubModule is not imported.

// @lnsFront: skip
print( val2 );

Also, the variables to be exposed to the outside have the following restrictions.

"Variables exposed externally must be declared in the topmost scope of the script"

For example, val2 below is an error because it is not in the topmost scope.

// @lnsFront: error
pub let val = 1;
{
   pub let val2 = 1; // error
}

mutable

A variable that is simply declared is treated as a variable that cannot be changed.

For example, val = 2 below is an error.

// @lnsFront: error
let val = 1;
val = 2; // error

If you want to make it a variable variable (mutable), declare it with mut as follows.

// @lnsFront: ok
let mut val = 1;
val = 2;

It is also possible to assign an initial value after declaring an immutable variable as follows.

// @lnsFront: ok
let val;
val = 1;

However, if you set another value after assigning the initial value as follows, an error will occur.

// @lnsFront: error
let val;
val = 1;
val = 2; // error

immutable types

As mentioned above, variables that are not declared mut are immutable. Inferred types without a mut declaration are also immutable. For example, in the following case, since list1 is declared mut , it is possible to change List (insert), but list2 is immutable without mut declaration, so the change operation of List will result in an error.

// @lnsFront: error
let mut list1 = [1];
list1.insert( 2 ); // ok
let list2 = [1];
list2.insert( 2 ); // error

An immutable type is denoted as &T by appending & to the original type T. For example, &List<int> represents a list List<int> that cannot be modified. In addition, change operation is not possible, but reference operation such as foreach is possible.

&List<List<int>> is an immutable list whose elements are List<int> . Here List<int> is mutable because there is no &. So it looks like this:

// @lnsFront: error
let list:&List<List<int>> = [[100],[]];
list[1].insert( 1 ); // ok
list.insert( [10] ); // error

Type inference and mutable

As mentioned above, the type of a variable that is not declared mut is immutable.

But this is with type inference.

Even a variable that is not declared mut depends on the mutable declaration of that type if the type is specified.

For example:

// @lnsFront: error
let list1:List<int> = [1,2];
let list2:&List<int> = [1,2];
let mut list3 = [1,2];
let list4 = [1,2];
list1.insert( 3 );
list2.insert( 3 ); // error
list3.insert( 3 );
list4.insert( 3 ); // error

list2.insert( 3 ); and list4.insert( 3 ); fail because list2 and list4 become immutable &List<int> .

There was a bug in this specification before, which has been fixed in ver 1.2.0. Previously, even if the type was specified, the type was immutable if mut was not declared, but the behavior was inferior for variables, members, and arguments, so it has been corrected to the current specification.

If you want to return to the specification before ver 1.2.0, please specify the option –legacy-mutable-control .

However, this option may be deprecated in the future.

multiple declarations

LuneScript is the same as Lua and can return multiple values in function return values.

To make this return value the initial value of a variable declaration, declare it like this:

// @lnsFront: skip
let val1, val2 = func();
let mut val3, mut val4 = func();

Declare mut before each variable name.

access check

If you declare a local variable and do not refer to it after setting its value, you will get a warning. On the other hand, class members and function arguments are not subject to access checks.

The following sample is an example of using only the second value without using the first value of the multi-value return. In this case, warn that val1 containing the first value is not used.

// @lnsFront: ok
fn sub(): int, int {
   return 1, 2;
}
fn func() {
   let val1, val2 = sub(); // warning val1
   print( val2 );
}

To suppress warnings for variables declared only to access the second and subsequent values of such a multi-value return, use the '_' symbol, like this:

// @lnsFront: ok
fn sub(): int, int {
   return 1, 2;
}
fn func() {
   let _, val2 = sub(); // ok
   print( val2 );
}

Note that variables declared with the '_' symbol cannot be accessed. Accessing it will result in an error.

// @lnsFront: error
fn sub(): int, int {
   return 1, 2;
}
fn func() {
   let _, val2 = sub();
   print( _ ); // error
   print( val2 );
}

Access checks are also performed after updating the value of a variable.

For example, val1 will be warned if:

// @lnsFront: ok
fn func() {
   let mut val1 = 1;
   print( val1 );
   val1 = 2; // warning
}

This is because val1 is referenced by print( val1 ) after setting 1 to val1, but val1 is not referenced after updating val1 with val1 = 2.

Closure access checks

This access check also works with closures.

In the following sample, val1 = 2 is followed by a sub() call, which determines that val1 is being referenced and does not warn.

// @lnsFront: ok
fn func() {
   let mut val1 = 1;
   fn sub() {
      print( val1 );
   }
   val1 = 2;
   sub();
}

However, there are the following restrictions.

  • Treat it as if there was a reference to the value at the point of reference, not the function call of the closure

    • For example, assigning a closure function to a variable or passing it as an argument to another function.
  • Closure access does not distinguish between references and settings

    • Even if it is only set in the closure function, it is treated as a reference.

Closure access checking is an experimental feature.

special symbol

The following symbols refer to special values.

symbol value
__mod__ Module name
__func__ current function name
__line__ current line number

Note that the format of names expanded by __mod__ and __func__ may change in the future.

Type conversion (cast)

Any value other than nil can be assigned to a variable of type stem .

It has an implicit type conversion.

// @lnsFront: ok
let mut val:stem = 1;
val = 1.0;
val = "abc";
val = {};
val = [];
val = [@];

On the other hand, it is an error to assign a stem type value to a different type.

// @lnsFront: error
let val1:stem = 1;
let val2:int = val1; // error

If you need explicit type conversions, see the following articles:

../cast

reference

Variables hold object references except for some (int, real, nil).

For example:

// @lnsFront: ok
let mut list1 = [ 10 ];
let list2 = list1;
list1.insert( 20 );
list1.insert( 30 );
foreach val in list2 {
   print( val ); // 10 20 30
}
  • Set List<int> type list ([ 10 ]) object reference to list1
  • Set the reference held by list1 to list2
  • insert 20, 30 into the list object referenced by list1
  • print() each value of the list object referenced by list2

Here, list1 and list2 refer to the same list object, so if you insert 20, 30 into list1 , print( val ) foreaching list2 will print 10 20 30 .

Also, if you insert 40 into list2 like this, print( val ) will print 10 20 30 40 because you are inserting 40 into the same list object.

// @lnsFront: ok
let mut list1 = [ 10 ];
let mut list2 = list1;
list1.insert( 20 );
list1.insert( 30 );
list2.insert( 40 );
foreach val in list2 {
   print( val ); // 10 20 30 40
}

If you set list1 to a new list object ([ 100]), print( val ) will print 10 20 30 40 because the list object referenced by list1 and the list object referenced by list2 are different.

// @lnsFront: ok
let mut list1 = [ 10 ];
let mut list2 = list1;
list1.insert( 20 );
list1.insert( 30 );
list2.insert( 40 );
list1 = [ 100 ];
foreach val in list2 {
   print( val ); // 10 20 30 40
}

The same is true for List<List<int>>.

// @lnsFront: ok
let mut list = [ 10, 20 ];
let mut wrapList:List<List<int>> = [];
wrapList.insert( list );
wrapList.insert( list );
wrapList.insert( [ 100, 200 ] );
list[ 1 ] = list[ 1 ] + 1;
print( wrapList[ 1 ][ 1 ], wrapList[ 1 ][ 2 ] ); // 11 20
print( wrapList[ 2 ][ 1 ], wrapList[ 2 ][ 2 ] ); // 11 20
print( wrapList[ 3 ][ 1 ], wrapList[ 3 ][ 2 ] ); // 100 200

Add list to wrapList 1st and 2nd, and add new list object to wrapList 3rd. After that, increment list[1] and output the contents of wrapList.

Here, wrapList[1][1] and wrapList[2][1] point to the same list[1], so the incremented value is output. wrapList[3] is a new list object, so the increment has no effect.

summary

Variables in LuneScript incorporate the following elements:

  • type inference
  • access control
  • mutable
  • multiple declarations

It is intended to meet the minimum functionality required when dealing with Lua with static cleanup.

Next time, I will explain the branch control of LuneScript.