Skip to content
s-hadinger edited this page Dec 21, 2021 · 16 revisions

7. Advanced features

7.1 strict mode

Berry allows full freedom from the developer. But after some experience in coding with Berry, you will find that there are common mistakes that are hard to find and that the compiler could help you catch. The strict mode does additional checks at compile time about some common mistakes.

This mode is enabled with import strict or when running Berry with -s option: berry -s

Mandatory var for local variables

This is the most common mistake, a variable assigned without var is either global if a global already exists or local otherwise. Strict mode rejects the assignment if there is no global with the same name.

No more allowed:

def f()
  i = 0    # this is a local variable
  var j = 0
end
syntax_error: stdin:2: strict: no global 'i', did you mean 'var i'?

But still works for globals:

g_i = 0
def f()
  g_i = 1
end

No overriding of builtins

Berry allows to override a builtin. This is however generally not desirable and a source of hard to find bugs.

map = 1
syntax_error: stdin:1: strict: redefinition of builtin 'map'

Multiple var with same name not allowed in same scope

Berry tolerated multiple declaration of a local variable with the same name. This is now considered as an error (even without strict mode).

def f()
  var a
  var a   # redefinition of a
end
syntax_error: stdin:3: redefinition of 'a'

No hiding of local variable from outer scope

In Berry you can declare local variables with the same name in inner scope. The variable in the inner scope hides the variable from outer scope for the duration of the scope.

The only exception is that variables starting with dot '.' can mask from outer scope. This is the case with hidden local variable .it when multiple for are embedded.

def f()
  var a    # variable in outer scope
  if a
    var a  # redefinition of a in inner scope
  end
end
syntax_error: stdin:4: strict: redefinition of 'a' from outer scope

7.2 Virtual members

Virtual members allows you to dynamically and programmatically add members and methods to classes and modules. You are no more limited to the members declared at creation time.

This feature is inspired from Python's __getattr__() / __setattr__(). The motivation comes from LVGL integration to Berry in Tasmota. The integration needs hundreds of constants in a module and thousands of methods mapped to C functions. Statically creation of attributes and methods does work but consumes a significant amount of code space.

This proposed features allows to create two methods:

Berry method Description
member (name:string) -> any
Should return the value of the specified name
setmember (name:string, value:any) -> nil
Should store the value to the virtual member with the specified name

implicit call of member()

When the following code a.b is executed, the Berry VM does the following:

  • Get the object named a (local or global), raise an exception if it doesn't exist
  • Check if the object a is of type module, instance or class. Raise an exception otherwise
  • Check if object a has a member called b. If yes, return its value, if no proceed below
  • If object a is of type class, raise an exception because virtual members do not work for static (class) methods
  • Check if object a has a member called member and it is a function. If yes call it with parameter "b" as string. If no, raise an exception

implicit call of setmember()

When the following code a.b = 0 (mutator) is executed, the Berry VM does the following:

  • Get the object named a (local or global), raise an exception if it doesn't exist
  • Check if the object a is of type module, instance or class. Raise an exception otherwise
    • If a is of type class, check if member b exists. If yes, change its value. If no, raise an exception. (virtual members don't work for classes or static methods)
    • If a is of type instance, check if member b exists. If yes, change its value. If no, proceed below
      • Check if a has a member called setmember. If yes call it, if no raise an exception
    • If a is of type module. If the module is not read-only, create of change the value (setmember is never called for a writable module). If the module is read-only, then setmember is called if it exists.

Exception handling

To indicate that a member does not exist, member() shall return nil. Hence it is not possible to to have a virtual member with a value nil.

You can also raise an exception in member() but be aware that Berry might try to call methods like tostring() that will land on your member() method if they don't exist as static methods.

To indicate that a member is invalid, setmember() should raise an exception.

Be aware that you may receive member names that are not valid Berry identifiers. The syntax a.("<->") will call a.member("<->") with a virtual member name that is not lexically valid, i.e. cannot be called in regular code, except by using indirect ways like introspect or member().

Specificities for classes

Access to members of class object do not trigger virtual members. Hence it is not possible to have virtual static methods.

Specificities for modules

Modules do support reading static members with member().

When writing to a member, the behavior depends whether the module is writable (in memory) or read-only (in firmware).

If the module is writable, the new members are added directly to the module and setmember() is never called.

If the module is read-only, then setmember() is called whenever you try to change or create a member. It is then your responsibility to store the values in a separate object like a global.

Example

Example:

class T
    var a
    def init()
        self.a = 'a'
    end

    def member(name)
        return "member "+name
    end

    def setmember(name, value)
        print("Set '"+name+"': "+str(value))
    end
end
t=T()

Now let's try it:

> t.a
'a'
> t.b
'member b'
> t.foo
'member foo'
> t.bar = 2
Set 'bar': 2

This works for modules too:

m = module()
m.a = 1
m.member = def (name)
    return "member "+name
end
m.setmember(name, value)
    print("Set '"+name+"': "+str(value))
end

Trying:

> m.a
1
> m.b
'member b'
> m.c = 3   # the allocation is valid so `setmember()` is not called
> m.c
3

More advanced example:

> class A
    var i
  
    def member(n)
      if n == 'ii' return self.i end
      return nil     # we make it explicit here, but this line is optional
    end

    def setmember(n, v)
      if n == 'ii' self.i = v end
    end
  end
> a=A()

> a.i      # returns nil
> a.ii     # implicitly calls `a.member("ii")`
attribute_error: the 'A' object has no attribute 'ii'
stack traceback:
	stdin:1: in function `main`
# returns an exception since the member is nil (considered is non-existant)

> a.ii = 42    # implicitly calls `a.setmember("ii", 42)`
> a.ii         # implicitly calls `a.member("ii")` and returns `42`
42
> a.i          # the concrete variable was changed too
42

7.3 How-to package a module

This guide drives you through the different options of packaging code for reuse using Berry's import directive.

Behavior of import

When you use import <module> [as <name>], the following steps happen:

  • There is a global cache of all modules already imported. If <module> was already imported, import returns the value in cache already returned by the first call to import. No other actions are taken.
  • import searches for a module of name <module> in the following order:
  1. in native modules embedded in the firmware at compile time
  2. in file system, starting with current directory, then iterating in all directories from sys.path: look for file <name>, then <name>.bec (compiled bytecode), then <name>.be. If BE_USE_SHARED_LIB is enabled, it also looks for shared libraries like <name>.so or <name>.dll although this optional is generally not available on MCUs.
  • The code loaded is executed. The code should finish with a return statement. The object returned is stored in the global cache and made available to caller (in local or global scope).
  • If the returned object is a module and if the module as a init member, then an extra step is taken. The function <module>.init(m) is called passing as argument the module object itself. The value returned by init() replaces the value in the global cache. Note that the init() is called at most once during the first import.

Packaging a module

Here is a simple example of a module:

File demo_module.be:

# simple module
# use `import demo_module`

demo_module = module("demo_module")

demo_module.foo = "bar"

demo_module.say_hello = def ()
    print("Hello Berry!")
end

return demo_module      # return the module as the output of import

Example of use:

> import demo_module

> demo_module
<module: demo_module>

> demo_module.say_hello()
Hello Berry!

> demo_module.foo
'bar'
> demo_module.foo = "baz"     # the module is writable, although this is highly discouraged
> demo_module.foo
'baz'

Package a singleton (monad)

The problem of using modules is that they don't have instance variables to keep track of data. They are essentially designed for state-less libraries.

Below you will find an elegant way of packaging a class singleton returned as an import statement.

To do this, we use different tricks. First we declare the class for the singleton as an inner class of a function, this prevents from polluting the global namespace with this class. I.e. the class will not be accessible by other code.

Second we declare a module init() function that creates the class, creates the instance and returns it.

By this scheme, import <module> actually returns an instance of a hidden class.

Example of demo_monad.be:

# simple monad
# use `import demo_monad`

demo_monad = module("demo_monad")

# the module has a single member `init()` and delegates everything to the inner class
demo_monad.init = def (m)
    
    # inncer class
    class my_monad
        var i

        def init()
            self.i = 0
        end

        def say_hello()
            print("Hello Berry!")
        end
    end

    # return a single instance for this class
    return my_monad()
end

return demo_monad      # return the module as the output of import, which is eventually replaced by the return value of 'init()'

Example:

> import demo_monad
> demo_monad
<instance: my_monad()>     # it's an instance not a module

> demo_monad.say_hello()
Hello Berry!

> demo_monad.i = 42        # you can use it like any instance
> demo_monad.i
42

> demo_monad.j = 0         # there is strong member checking compared to modules
attribute_error: class 'my_monad' cannot assign to attribute 'j'
stack traceback:
	stdin:1: in function `main`

7.4 Solidification

Clone this wiki locally