Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a draft on programmatic code manipulation #349

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions General/ProgrammaticCodeManipulation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Programmatic Code Manipulation (Pharo 11)

In this article, we will present some code snippets that demonstrate how we can manipulate the source code (packages, classes, methods, variables, traits, etc.) programmatically, without using the SystemBrowser. Those techniques can be very useful for reflective operations, code generation, testing, etc.

This article explains only those operations that **modify** the code. We will write a separate article on **querrying code** (looking for method senders, class references, pragma usages, etc.).

## Packages

### Creating a new package

```st
package := self packageOrganizer ensurePackage: 'PackageName'.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is P12 only so maybe the name of the article should be renamed?

```

In Playground, you can use any object or class instead of `self`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The playground have a self so we can use the same code.


```st
package := Object packageOrganizer ensurePackage: 'PackageName'.
```

### Removing a package

```st
package removeFromSystem.
```

**Be careful!** In Pharo 11 and earlier, this will not properly remove classes from the system. So make sure to remove all classes before you remove the package.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Pharo 12 we can also do:

self packageOrganizer removePackage: 'PackageName'

#removePackage: can take:

  • A package name and remove it
  • A real package and remove it
  • A name of a package that does not exist and will silently do nothing in that case.

I use this a lot to create teardowns in tests because maybe the test failed directly and no package was generated. In that case I don't want the tear down to crash.


### Creating a package without installing it

_(how to create an instance of a package without installing it into the image? - e.g., for testing)_
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By definition we cannot because currently we consider that code is installed if it is in a package.

If you want to manipulate packages that are not in the global organizer you can create a new organizer:

PackageOrganizer new

(Note that this name exist only in P12+)

Or better, create a new environment that will have a new package organizer (because the environment of an environmentless organizer will still be the global environment)

SystemDictionary new

Or to avoid hard references:

self class environment class new


## Classes

### Creating a new class

To create a new class, you can use the same code that appears in SystemBrowser. Here is an example with **Fluid** class definition.

```st
builder := Object << #MyClass slots: { }; package: 'MyPackage'. class := builder install.
```

The first statement will return an instance of `FluidClassBuilder`. The second statement will build the class, install it into the image, and assign it to the variable. If `MyPackage` does not exist, it will be created.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is false in P12.

We had:

  • FluidClassBuilder
  • ShiftClassBuilder
  • ShiftClassInstaller

FluidClassBuilder was created with the fluid syntax. When you wanted to install it, you had the create a ShiftClassInstaller that was delegating the building to the ShiftClassBuilder.

But this had some flaws:

  • The FluidClassBuilder could not manage to build a class with a name for the superclass instead of a class object. This was bad for code generation because it meant that the superclass had to exist when you wanted to define it (even if you did not build anything)
  • The ShiftClassBuilder did not had a lot of API and some other API was complex because only used internally by the installer
  • The API of both was kind of close but not exactly the same and this was confusing

In P12 I merged both of them inside the ShiftClassBuilder (There is no FluidClassBuilder anymore). And I tried to make sure both strength were kept.


### Creating a new class without installing it

Here is an example of how you can create a new class without installing it into the system. You can do that by sending `build` message instead of `install` as in the previous example.

```st
builder := OrderedCollection << #MyCollection.myCollectionClass := builder build.myCollectionClass compile: 'generate: aNumber "Generate aNumber random integers" | rand | rand := Random new. aNumber timesRepeat: [ self add: (rand nextInteger: 100) ]'. myCollection := myCollectionClass new.myCollection generate: 4. "a MyCollection(60 5 40 11)"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is weird on Github I don't see the lines returns well. I hope it's just a display problem.

image

```

This way, we can create two classes that have a same name but do different things. In the example below, we create two classes named `Dog`. One class defines method `bark` to return the string `'Woof, woof!'` (English barking), the other one - `'Ouaf, ouaf !'` (French barking). As you can see below, the instances of those classes do different things. The classes are different but the class names are the same.

```st
dogClass1 := (Object << #Dog) build.dogClass2 := (Object << #Dog) build.dogClass1 compile: 'bark "Bark in English" ^ ''Woof, woof!'''.dogClass2 compile: 'bark "Bark in French" ^ ''Ouaf, ouaf !'''.dog1 := dogClass1 new.dog2 := dogClass2 new.dog1 bark. "'Woof, woof!'"dog2 bark. "'Ouaf, ouaf !'"

dog1 class = dog2 class. "false"dog1 class name = dog2 class name. "true"
```

**Be careful!** In Pharo 11, if the class named `Dog` already exists in the system, your new classes will also have the methods that the system class defines. However, the new methods will override the system ones in your new class.

For example, if there already was a class Dog in your image defined as follows:

```st
Object << #Dog slots: {}; package: 'MyPackage'

Dog >> printOn: aStream aStream nextPutAll: 'I am a dog!'.

Dog >> bark "Bark in Ukrainian" ^ 'Гав, гав!'
```

You can still define the local class as in the example above. In this case, your new class will understand the method `printOn:` from the system class and will define a new method `bark`:

```st
dog1. "I am a dog"
dog1 bark. "Woof, woof!"
```

### Creating an anonymous class

### Removing a class

This will also remove all methods of the class.

```st
class removeFromSystem.
```

## Methods

### Adding a method to a class

To add a new method to a class, you can simply write its source code (including method name and arguments) in a string and send it as argument of a `compile:` method of your class. Here is an example of adding the method `printOn:` to `MyClass` (this is a silly implementation that will print a class name followed by a random number).

```st
sourceCode := 'printOn: aStream "Print the object of this class" aStream nextPutAll: self class name; space; nextPutAll: (Random new nextInteger: 100) asString'.

MyClass compile: sourceCode.
```

You can also specify a protocol:

```st
MyClass compile: sourceCode classified: 'printing'.
```

Finally, to avoid the error when generating a code in a fresh image, you should specify the author name. It can be anything, so in this case, we use `'Dummy Name'`

```st
Author
useAuthor: 'Dummy Name'
during: [ MyClass compile: sourceCode ].
```

### Removing a method from a class

```st
method removeFromSystem.
```

## Variables

## Traits

### Adding trait to a class

### Removing trait from a class

### Creating a new trait

### Adding methods to a trait

## Useful links

- A toolkit to generate Pharo code: <https://github.com/juliendelplanque/PharoCodeGenerator>
Expand Down