Mate in 10 Minutes: Fuzzing Object Graphs

Common fuzzers generate bytes, strings, or structured grammar-based inputs. Pharo programs, however, work with objects: a BlElement with children that point back to their parent, a Player whose level must stay between 0 and 99, a model where some fields may be nil and others never are.

Mate is a small language and toolkit for describing those object graphs. From one description you can:

  • Generate valid objects, as many as you want, for testing and fuzzing;
  • Guide generation with feedback, so it explores the shapes that matter;
  • Validate existing objects against the description;
  • Infer a description from real objects or from your test suite;
  • Visualize how generation explored the grammar.

This guide walks through each use with small examples.


Installing

Metacello new
baseline: 'Mate';
repository: 'github://FedeLoch/Mate:main';
onConflictUseIncoming;
load.

Your first object grammar

Suppose we have a small game model:

Object << #Player
slots: { #name . #characterClass . #level . #missingExperienceForNextLevel };
package: 'MyGame'.

characterClass holds a Warrior, a Rogue or a Mage.

A Mate grammar says which classes each field may hold and which values are acceptable:

Player -> {
name: ByteString,
characterClass: Warrior | Rogue | Mage,
level: { type: SmallInteger, between: 0 and: 99 },
missingExperienceForNextLevel: { type: SmallInteger, greaterThan: 0 }
}

Read it as: a Player has a name that is a string, a characterClass that is one of three classes, a level from 0 to 99, and a strictly positive amount of missing experience.

To use it, subclass MateOGGrammar and return the description from defineObjectGraph:

MateOGGrammar << #PlayerGrammar
package: 'MyGame'.
PlayerGrammar >> defineObjectGraph [
^ 'Player -> {
name: ByteString,
characterClass: Warrior | Rogue | Mage,
level: { type: SmallInteger, between: 0 and: 99 },
missingExperienceForNextLevel: { type: SmallInteger, greaterThan: 0 }
}'
]

You can also load a grammar from a file:

grammar := MateOGGrammar fromFile: 'player.mate'.

Generating objects

grammar := PlayerGrammar new.
player := grammar gen: Player.
player level. "=> always between 0 and 99"
player characterClass. "=> a Warrior, a Rogue or a Mage"

Need a hundred?

players := (1 to: 100) collect: [ :i | grammar gen: Player ].

Values built for finding bugs

Mate does not pick values uniformly. It favours the values that tend to break code:

TypeWhat you get
Integers0, ±1, powers of two, 32 and 64-bit limits, SmallInteger minVal / maxVal, the edges of your constraints, plus small and random values
Floats0.0, -0.0, ±1.0, tiny and huge values, NaN and ±infinity when the declared type is Float, and the edges of your constraints
Stringsempty, short, very long, punctuation, control bytes, and Unicode for WideString

Constraints are always respected: a level declared between: 0 and: 99 will be 0, 1, 98, 99 and everything in between, but never 100.

Reproducible runs

When a generated object crashes your code, you want to get it back. Give the grammar a seed:

grammar := PlayerGrammar new.
grammar seed: 1234.
players := (1 to: 100) collect: [ :i | grammar gen: Player ].

The same seed produces the same sequence of objects.


The language at a glance

Here is a grammar for a hypothetical shop model that uses most of the language:

"Comments go between double quotes"
Shop -> {
name: ByteString,
rating: { type: Float, between: 0.0 and: 5.0 },
stock: { type: SmallInteger, atLeast: 0 },
status: { oneOf: { #open . #closed . #moved } },
website: ByteString?,
logo: (PNGImage | JPEGImage)?,
products: { type: [ Product ], sizeBetween: 0 and: 20 },
owner: { refType: Person },
selfReferences: { products[ .shop ] }
}
You writeIt means
A | Bthe field holds an A or a B
T?the field may also be nil
between: a and: b, atLeast:, atMost:numeric bounds, inclusive
greaterThan:, lessThan:numeric bounds, exclusive
in: [ 200, 404, 500 ]the value is one of these literals
oneOf: { #open . #closed }the value is one of these literals (symbols, strings, numbers, booleans, nil or classes)
{ type: [ T ], sizeBetween: a and: b }an Array of T, with a size range (size: n for an exact size, 0 to 10 by default)
refType: Ta back-pointer, filled in by selfReferences instead of being generated
selfReferences: { products[ .shop ] }after building a Shop, set the shop field of each of its products to that shop
X classthe class object itself (a metaclass type)

Relationships between objects

Real object graphs are not trees. In Bloc, every child points back to its parent:

BlElement -> {
children: BlChildrenArray,
parent: { refType: BlElement },
visuals: BlCustomVisuals | BlDefaultVisuals,
constraints: BlLayoutCommonConstraints,
selfReferences: { children.array[ .parent ] }
}
BlChildrenArray -> {
array: { type: [ BlElement ], sizeBetween: 0 and: 10 }
}

children.array[ .parent ] reads: follow children, then array, and for each element set its parent to me. Every generated element tree is therefore consistent: child parent == element holds for every child.

Recursion

Grammars can be recursive (BlElement contains BlElements). Generation stops at a maximum depth (12 by default). At that depth, a field takes a simple value if its type allows one, and nil otherwise. You can tune this and how often nullable fields are nil:

grammar generator
maxDerivationHeight: 6;
nilProbability: 0.2.

Let’s stop for 5 seconds to drink a mate🧉… Let’s continue! 🙂

Guided fuzzing: Teaching the grammar what is interesting

Random generation finds shallow bugs. To go deeper, tell Mate which objects were interesting: they reached new code, were slow, or produced a bigger difference in the output. Mate keeps a reward for every choice it made while building that object (which union option, which value, which size) and favours the choices that paid off, while still exploring the rest. Under the hood, this is the UCT formula from Monte Carlo Tree Search.

grammar := PlayerGrammar new.
grammar seed: 42.
1 to: 1000 do: [ :i |
| player score |
player := grammar gen: Player.
score := self newCoverageReachedBy: player.
grammar backPropagate: score from: player ].

backPropagate:from: takes a generated object, works out its derivation, and rewards every choice in it. If you already have the derivation tree, use backpropagate:improvement: directly:

derivation := grammar gen: Player from: MateContext new.
player := derivation instance.
grammar backpropagate: derivation improvement: 42.

Mutating part of an object

Sometimes you want to keep an object and regenerate only one part of it. Take a node of the derivation tree, generate a variant, and swap it in. References such as parent are kept consistent:

derivation := grammar gen: BlElement from: MateContext new.
node := derivation edges at: 'children'.
variant := grammar genVariant: node.
grammar replace: node by: variant.

How much of the grammar did we explore?

grammar explorationPercentage. "=> fraction of the grammar visited so far"
grammar explorationPercentageFrom: Player. "=> the same, counting only what is reachable from Player"

Validating objects

The same grammar can check objects that come from anywhere: your application, a file, a test.

grammar := PlayerGrammar new.
grammar parse: (Player new
name: 'Fede';
level: 42;
characterClass: Rogue new;
missingExperienceForNextLevel: 1).
"=> a derivation tree: the object is valid"
grammar parse: (Player new
name: 'Fede';
level: 500;
characterClass: Rogue new;
missingExperienceForNextLevel: 1).
"=> Error: Value 500 of type SmallInteger violates MateConstraint( min: 0 max: 99)"

parse: checks field types (subclasses are accepted), array sizes, value constraints, and selfReferences. It never modifies the object, and it handles cyclic graphs.

This makes Mate useful as an oracle too: generate objects, run an operation on them, and check that the result still matches the grammar.


Inferring a grammar from real objects

Writing a grammar by hand is not always practical. Mate can infer one from objects you already have:

grammar := MateOGGrammar fromObjects: { player1 . player2 . player3 }.
grammar mateDefinition.
Player -> {
name: ByteString,
characterClass: Mage | Rogue | Warrior,
level: SmallInteger,
missingExperienceForNextLevel: SmallInteger
}

Mate merges what it sees across all instances: field types become unions, arrays get the element types and size ranges observed, booleans become Boolean, and fields that hold classes become oneOf choices.

You can also infer a grammar from the objects your test suite builds:

grammar := MateOGGrammar fromTest: PlayerTest.

Every test is set up and run, and the objects held in its instance variables, plus its return value, are fed to the inference. Failing tests are skipped.

Inferred grammars are a starting point: they know the types but not yet the constraints (ranges, relationships). Edit the output of mateDefinition, which can be parsed back with no changes, and turn it into your hand-written grammar.


Seeing what happened

Mate uses Roassal to draw two things.

A derivation tree: how one object was built, with every choice made.

(PlayerGrammar new gen: Player from: MateContext new) render.

The grammar as a heat map: which declarations and options were visited most during generation.

grammar := MateBlocGrammar new.
100 timesRepeat: [ grammar gen: BlElement ].
grammar render.

Cheat sheet

I want to…Code
generate one objectgrammar gen: Player
generate reproduciblygrammar seed: 42
get the derivation tree toogrammar gen: Player from: MateContext new
reward an interesting objectgrammar backPropagate: score from: player
regenerate part of an objectgrammar replace: node by: (grammar genVariant: node)
check an objectgrammar parse: anObject
infer a grammarMateOGGrammar fromObjects: objects or MateOGGrammar fromTest: aTestClass
print a grammargrammar mateDefinition
load a grammar from a fileMateOGGrammar fromFile: 'grammar.mate'
measure explorationgrammar explorationPercentageFrom: Player
visualizederivation render, grammar render
limit recursiongrammar generator maxDerivationHeight: 6

Thanks for reading!! 🙂

Learn more in the repository and the other pages of this wiki.

Shin: Shrinking inputs in Pharo

You found an input that breaks your program, for example, a 3 KB JSON document, or a 200-character regex, or an SVG path that makes the renderer hang. Before you can debug it, you want the smallest input that still shows the problem, right?

That is what Shin does. You give it an input and a way to recognize the problem, and it removes everything that doesn’t matter.

shrinker := ShinDDminPaperShrinker new.
shrinker oracle: (ShinBooleanInputOracle from: [ :string | string includes: $e ]).
shrinker shrink: 'asqjjkqsbdvieqvsbdoivbq'. "=> 'e'"

Shin is a Pharo library with self-contained implementations of the main input-reduction algorithms (delta debugging, HDD, Perses-like grammar reduction, Nautilus, Vulcan, ProbDD, CDD, WDD, SADD…), plus a small framework to benchmark them against each other.


Installation

Metacello new
baseline: 'Shin';
repository: 'github://FedeLoch/Shin:main';
onConflictUseIncoming;
load.

This also loads Gnocco (grammars and parsing), NeoJSON, and PBA. Benchmark plots use Roassal, which ships with Pharo.


Three concepts: Shrinker, Oracle, and Grammar

ConceptQuestion it answersExample
ShrinkerHow do I make the input smaller?ShinDDminPaperShrinker, ShinHDDShrinker, ShinVulcanShrinker
OracleDoes this smaller input still show the problem?“it raises ZeroDivide“, “it covers the same code”
GrammarWhat does a valid input look like? (optional)AritExpressionGrammar, SvgPathGrammar

The three are independent: any oracle works with any shrinker, and a grammar is only needed by the tree-based shrinkers. That’s what makes it easy to compare algorithms on exactly the same problem.

Every shrinker is used the same way:

shrinker := SomeShrinker new.
shrinker grammar: aGrammar. "only for grammar-based shrinkers"
shrinker oracle: anOracle.
smallest := shrinker shrink: input.

The result always still satisfies the oracle.


Shrinking strings and collections

Flat shrinkers see the input as a sequence (a String or any Collection) and remove elements. They know nothing about syntax.

shrinker := ShinDDminFuzzingBookShrinker new.
shrinker oracle: (ShinBooleanInputOracle from: [ :s |
(s includes: $e) and: [ s includes: $a ] ]).
shrinker shrink: 'kjdsfhkjqsdhfehdkjsqfhkjsa'. "=> 'ea'"

Each element is handled independently, so repeated elements are not a problem:

shrinker oracle: (ShinBooleanInputOracle from: [ :s | s includesSubstring: 'bug' ]).
shrinker shrink: 'zzbzzuzzgzzbugzz'. "=> 'bug'"

Collections other than strings keep their class:

shrinker oracle: (ShinBooleanInputOracle from: [ :array | array includes: 4 ]).
shrinker shrink: #(1 2 3 4 5 6 7 8). "=> #(4)"

Flat shrinkers available: ShinDDminPaperShrinker, ShinDDminFuzzingBookShrinker, ShinProbDDShrinker, ShinCDDShrinker, ShinWDDminShrinker, ShinWDDProbShrinker.

The catch: a flat shrinker happily produces inputs that are no longer syntactically valid. If your program rejects them early, the oracle says “no”, and the reduction stalls. For structured inputs, use a grammar.


Shrinking structured grammar-based inputs

Grammar-based shrinkers parse the input into a tree and reduce the tree. Every candidate they try is valid according to the grammar, so the oracle spends its time on meaningful inputs.

Using a bundled grammar

Shin ships an arithmetic-expression grammar. Let’s find the smallest expression that still divides by zero:

shrinker := ShinVulcanShrinker new.
shrinker grammar: AritExpressionGrammar new.
shrinker oracle: ShinDivideZeroOracle new.
shrinker shrink: '((8500/(208000*80))+(165468468)*(423-12)*(999/(12+88)))/(((145*4)-580)*3)+((456)*12)-(89/(45+(12*8)))+(77*14)'.

The benchmarks also use grammars for regexes (GncRegexGrammar), JSON (JSONGrammar), Markdown/Microdown (ExtendedMarkdownGrammar), SVG paths (SvgPathGrammar) and Roassal graph scripts (RoassalGraphGrammar).

Writing your own grammar

Grammars are Gnocco grammars: a subclass of GncGrammar with one slot per non-terminal (names start with nt) and a defineGrammar method. Here is a grammar for lists of numbers such as [12,7,345]:

GncGrammar << #NumberListGrammar
slots: { #ntStart. #ntItems. #ntNumber. #ntDigit };
package: 'MyProject'.
NumberListGrammar >> defineGrammar [
ntStart --> '[' , ']' | '[' , ntItems , ']'.
ntItems --> ntNumber | ntNumber , ',' , ntItems.
ntNumber --> ntDigit | ntDigit , ntNumber.
ntDigit --> ($0 - $9).
^ ntStart
]

--> defines the rules of a non-terminal, | separates alternatives, , concatenates, strings are literals, and ($0 - $9) is a character range.

Now shrink a list while keeping two equal numbers:

shrinker := ShinHDDShrinker new.
shrinker grammar: NumberListGrammar new.
shrinker oracle: (ShinBooleanInputOracle from: [ :s |
| numbers |
numbers := (s copyFrom: 2 to: s size - 1) substrings: ','.
numbers size ~= numbers asSet size ]).
shrinker shrink: '[12,7,345,8,7,90]'. "=> '[7,7]'"

Every intermediate candidate is a well-formed list, and so is the result.

Shin Grammar-based shrinkers

ShrinkerIdea
ShinHDDShrinker / ShinHDDPaperShrinkerHierarchical Delta Debugging: ddmin applied level by level on the tree.
ShinGRABRShrinkerReplaces subtrees by smaller subtrees (or alternatives) of the same symbol, Perses style.
ShinNautilusShrinkerReplaces subtrees with the smallest derivation of their symbol.
ShinVulcanShrinkerHDD followed by auxiliary reducers: identifier replacement, subtree replacement, and tree-based local exhaustive enumeration.
ShinSADDminShrinker, ShinSAProbDDShrinkerStructure-aware ddmin / ProbDD: elements weighted by their position and shape in the tree.
ShinSADDminHDDShrinker, ShinSAProbDDHDDShrinkerThe two above, applied level by level inside HDD.

Oracles: Deciding what “still fails” means

The oracle is where you encode what you are preserving. Shin evaluates the original input once to get a baseline, then asks the oracle, for every candidate, whether it still matches that baseline.

Ready-made oracles

OracleKeeps inputs that…
ShinBooleanInputOracle from: aBlockmake the block answer true. The everyday choice.
ShinDivideZeroOracleraise ZeroDivide when evaluated as Pharo code.
ShinExecutionTimeOracletake at least threshold × the original execution time (default 95%).
ShinMethodCallsOraclecall at least threshold × as many instrumented methods as the original (default 95%).
ShinCoverageOraclecover exactly the same code as the original.
ShinFeedbackTableOraclekeep a PBA profiling score at least threshold × the original.

Boolean oracles

ShinBooleanInputOracle from: [ :input | [ MyParser parse: input. false ]
on: MyParserError do: [ true ] ]

Catch the error you are looking for inside the block, as above. Grammar-based shrinkers treat any other exception as “the problem disappeared”, but flat shrinkers let it propagate, and the reduction stops.

Preserving code coverage

Useful to find a small input that still exercises the same code, for instance to turn a fuzzer finding into a readable test:

oracle := ShinCoverageOracle new
methodsToInstrument: RxMatcher package methods;
evalBlock: [ :regex | regex asRegex matches: regex minimalStringMatching ];
yourself.
shrinker := ShinDDminFuzzingBookShrinker new oracle: oracle; yourself.
shrinker shrink: 'b+bc||'.

The coverage oracle instruments the methods with metalinks once per reduction, and removes them when it finishes (even if it is interrupted).

Preserving a performance problem

oracle := ShinExecutionTimeOracle new
evalBlock: [ :input | MySlowParser parse: input ];
threshold: 0.9;
yourself.

Candidates are kept as long as they stay at least 90% as slow as the original.

Writing your own oracle

Subclass ShinInputOracle (or ShinThresholdOracle for numeric scores) and implement:

MethodResponsibility
evalInput: inputMeasure the input (a Boolean, a number, a set…). Used to build the baseline.
accepts: input from: baselineAnswer whether input still shows the problem compared to baseline.
valueBound: aBoundTurn a measure into a number, used for the “% problem preservation” statistic.
during: aBlock (optional)Do an expensive setup/teardown once around a whole reduction.
ShinInputOracle << #LongestLineOracle
package: 'MyProject'.
LongestLineOracle >> evalInput: input [
^ (input lines collect: #size) inject: 0 into: [ :a :b | a max: b ]
]
LongestLineOracle >> accepts: input from: baseline [
^ (self evalInput: input) = baseline
]
LongestLineOracle >> valueBound: aBound [
^ aBound
]

Thanks a lot for reading! 🙂

Source code, issues, and datasets: https://github.com/FedeLoch/Shin

Creating a Pharo application for MacOS

Packaging your code as an application is an important point. It is the last step that makes a difference between a bunch of packages and a complete application. 

Thanks to the beautiful work in the PharoApplicationGenerator package, developed by Pablo Tesone, it is now possible to quickly generate an application and an .dmg installer, specifying your Metacello baseline, so you can have an executable for your application.

Now you can use a one-line command to build an installer for your own Pharo macOS application.

If you just want the sample command, this is it:

wget -O- get.pharo.org/64/130+vm | bash; ./pharo Pharo.image metacello install github://hernanmd/the-note-taker/src BaselineOfTheNoteTaker --groups=Release; ./pharo Pharo.image eval "NTCommandLineHandler generateApplication"; chmod 755 build/build.sh; cd build; ./build.sh; open NoteTaker.app

Let’s dive into each step of this one-liner. We will create an installer for a sample application for the Note Taker project:

Downloading Pharo

wget -O- get.pharo.org/64/130+vm | bash

This part downloads Pharo 13.0 (64-bit) and its Virtual Machine (VM). The output is piped directly to bash for execution.

Installing the Project

./pharo Pharo.image metacello install github://hernanmd/the-note-taker/src BaselineOfTheNoteTaker --groups=Release

We’re using Metacello (Pharo’s package manager) to install “The Note Taker” project from its GitHub repository, specifically the Release group. The Release group should include a package that will be only loaded for the executable generation process and includes an NTCommandLineHandler class used to generate a build shell script.

Generating the Application

./pharo Pharo.image eval "NTCommandLineHandler generateApplication"

This step invokes a custom command line handler to generate the application. It’s worth noting that NTCommandLineHandler is a subclass of CommandLineHandler, and CommandLineHandler is already provided in vanilla Pharo images. You should implement a very simple generateApplication class method in your own “NTCommandLineHandler“. Check an example implementation for macOS in the project’s GitHub repository. (https://github.com/hernanmd/the-note-taker/blob/master/src/TheNoteTaker-Release/NTCommandLineHandler.class.st)

Here is the gist of the command line handler

NTCommandLineHandler class >> generateApplication 

  (self environment at: #AppGeneratorOSXGenerator) new
    properties: { 
    #AppName -> 'NoteTaker' .
    #InfoString -> 'A note taking application written in Pharo' .
    #BundleIdentifier -> 'org.pharo.notetaker' .
    #ShortVersion -> '1.0.0' .
    #DisplayName -> 'Note Taker' .
    #CommandLineHandler -> 'noteTaker' .
    #CompanyName -> 'INRIA' .
      #VMType -> 'Spur' } asDictionary;
    outputDirectory: FileLocator workingDirectory / 'build';
    generate.

NTCommandLineHandler >> activate

   AppGeneratorSupport errorHandler: AppGeneratorSDLMessageErrorHandler new.
   OSWindowDriver current startUp: true.

   OSPlatform current isMacOSX
    ifTrue: [ | main |
        main := CocoaMenu new.
        main    title: 'MainMenu'; "Only informative"
            addSubmenu: 'Application' with: [ :m |
                m
                    addItemWithTitle: 'Quit'
                    action: [ Smalltalk snapshot: false andQuit: true ]
                    shortcut: 'q';

                    addItemWithTitle: 'Restart'
                    action: [ NTSpApplication new start ]
                    shortcut: 's' ];
            addSubmenu: 'Help' with: [ :m |
                m
                    addItemWithTitle: 'Show Help'
                    action: [ self inform: 'Help' ]
                    shortcut: '' ].
        main setAsMainMenu.
        NTSpApplication new startFullScreen ]
    ifFalse: [ self inform: 'Not OSX' ].

Preparing the Build Script

chmod 755 build/build.sh

This command makes the build script executable.

Building the Application

cd build; ./build.sh

We change to the build directory and execute the build script. It’s important to note that the build.sh script requires the create-dmg package, which you can install via Homebrew (brew install create-dmg). This dependency is crucial for creating the macOS disk image (DMG) file.

Opening the Application

open NoteTaker.app

Finally, we will open the newly created NoteTaker application.

Conclusion

This command line showcases the power and flexibility of Pharo’s development ecosystem. It demonstrates how we can automate the entire process from downloading Pharo to building and launching a desktop application, all in one command. In future posts, we will explore how to expand this process for GNU/Linux and Windows systems.

Next Pharo Sprints @ Lille

For those interested, we are organizing monthly Pharo sprints in Lille, in the Inria Research Center every last friday of the month! Tell us if you would like to come!

What’s a Pharo Sprint?

Imagine coding in something fun. And having cookies. Pair-programming with a guy that does something completely different than you in your every-day job. And candies. Some coffee too.

Well, that’s the best I can describe a Pharo sprint :).

Organised usually internally in the Evref Inria team @ Lille, we talked about opening to anybody that would like to join! We can connect on discord and do some remote sessions.

The goal? Could be fixing bugs. Could be improving some part of Pharo. Could be just learn something new. You choose.

When?

These are the following Sprint dates:

  • 29/09
  • 27/10
  • 24/11
  • 22/12

If you want to come, find the address below, and please keep us updated on discord or mail so we can organise it better!

INRIA – Avenue Halley,
Parc Scientifique de la Haute Borne,
Bat.A, Park Plaza
France

G

Pharo @ ESUG’23

The yearly European Smalltalk User Group conference happened last week in Lyon, France.
Unofficial numbers tells it had more than 80 participants, and we had met with lots of people we hadn’t seen for years!
We are slowly recovering from COVID 🙂

Of course, you can find the full conference info and schedule in here: https://esug.github.io/2023-Conference/agenda/agenda.html.
However, the goal of this post is to be short and give some highlights on the Pharo activity @ESUG, to keep you updated and steer some discussions.

The Slides of all talks are already in the ESUG Archive, videos will be online soon. I’ll update this post when that happens!

Besides the typical news around Pharo, the announcement of Pharo11 release (from earlier this year), we have seen lot of work on tooling, AI, Databases, web frameworks, infrastructure, and people using Pharo to build different kind of (maybe even crazy in some cases) applications.

Infrastructure

Pharo 11 has improvements in the bytecode compiler, block closure optimizations, Ephemeron support.
Git integration has been stable and working during years, and we keep you updated with the latest libgit releases.

Packaging Pharo desktop applications is being improved a lot.
This even opens the door to build fancy desktop applications using the new versions of Bloc and Roassal!

Profiling and tooling

Pharo has seen in the last year lots improvements in the debugger infrastructure and the refactoring engine.
There is also interesting advances in program instrumentation, used for example to build memory consumption profilers.
There is also a fuzzing framework under development to perform automatic detection of bugs!

AI and BioInformatics

Pharo keeps moving in the AI front with performance improvements, library extensions and bindings to external libraries!
Lapack integration, better dataframes, fancy algorithms and numerical libraries.
All these power toolkits like Bio Smalltalk, which has lots of tools to do exploratory analysis on DNA and other biological analyses.
Imagine all these with the power of live debugging and interactive visualizations!

Databases

Besides the fancy and stable gemstone object databases that can iteract with Pharo, new things are appearing in this area.
Soil is a new object database written in Pharo, with fresh views on old concepts.
It’s even being used in production and apparently we can build sexy query systems on top!

Web Frameworks

The Pharo community keeps delivering high quality frameworks to help you build web applications.
Pharo JS has been updated for the latest Pharo and latest ECMA.
Also, the guys from Yesplan have integrated the Hotwire framework from Ruby-on-Rails into Seaside!

Applications in Pharo!

Of course, not only Pharo is changing, but it also allows people to do amazing applications with it.
We have seen amazing demos of custom desktop applications by the people from Thales, native UIs and command line tools by the people from SCHMIDT, super fun simulators using Cormas.
Also a big heads up for the amazing Glamourous toolkit built on top of Pharo that announced its v1.0 release during the conf!

Community and fun stuff

We had also slots to discuss about the Google Summer of Code results with Pharo, where students did an amazing job on the Pharo IDE, Roassal, the charting libraries, the virtual machine…
Also, we had some interesting and fun community input on the current IDE thanks to the YesPlan people that run a survey and presented the results.
Domenico (Lucrezio), the crazy DJ that makes live coding music, showed his work on synthetizing music from Pharo, now with integrated visualizations!
Finally, there was an announcement on a new MOOC on Advanced Object-Oriented Design.

Conclusion

Of course, there were also lots of nice projects presented in the show us your projects, in the technology awards, in the research workshop.
Put on top the coffee break discussions, the social events, and the nights out!

ESUG’23 was a success, and it was lots of fun.
Let’s hope ESUG’24 will be as fun as this one too!

See you around!
Guille

Writing benchmarks with SMark

Introduction

SMark is a benchmarking framework for Pharo written originally by Stefan Marr. It serves as an essential tool for Pharo developers, enabling them to benchmark their code, identify performance bottlenecks, and make informed decisions to enhance their software’s efficiency.

It is composed of four main components: A Suite (which represents the benchmarks), a Runner (responsible to execute benchmarks), a Reporter (which knows how and what data to report) and a Timer.

SMark behaves similar to the testing framework SUnit with setUp and tearDown methods. And the runner can do what it wants/needs to reach warmup. For instance, the SMarkCogRunner will make sure that all code is compiled before starting to measure. Both warmup and setup/teardown methods can be specified per-benchmark.

Installation

You can install SMark in Pharo evaluating the following expression:

Metacello new
    baseline: 'SMark';
    repository: 'github://guillep/SMark';
    load.

How to implement a benchmark

Create a subclass of SMarkSuite. Inheritance from SMarkSuite is not a requirement but a convenient way to use the multiple hooks which the Suite offers.

SMarkSuite subclass: #MyBenchmarkSuite
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'MyProject'

Add a method named #bench<MyBenchmark>, like: #benchFactorial. This will be your real benchmark method. For example, a simple case repeating the benchmarking task would look like:

benchMyBenchmark

	self problemSize timesRepeat: [ … ]

and a benchmark which uses an index variable on each iteration, and the problem size:

benchMyBenchmark

	| i |
	i := self problemSize.
	[ i > 0 ] whileTrue: [
		" Some task using i "
		i := i - 1 ].

Hooks

There are multiple optional hooks to implement in your benchmark suite:

Define number of iterations in class side:

MyBenchmarkSuite class>>defaultNumberOfIterations
	^ 50

Define number of processes in class side:

MyBenchmarkSuite class>>defaultNumberOfProcesses
	^ 8

Define problem size in class side

MyBenchmarkSuite class>>defaultProblemSize
	^ 30

You can also override MyBenchmarkSuite>>setUp to set up the necessary environment for a benchmark and, if a specific benchmarks in your suite needs additional configuration, then create a method named MyBenchmarkSuite>>setUpBench<yourBenchmarkName> to set up the custom configuration (for example if you are benchmarking benchRegexDNA then you can create a setUpBenchRegexDNA if necessary).

Overriding MyBenchmarkSuite>>processResult:withTimer: will allow you to access the timer after a benchmark execution. And do not forget to override MyBenchmarkSuite>>tearDown to clean up the environment after a benchmark.

How to run benchmarks

To run a benchmark suite directly from Pharo, let’s say for example performing 100 Iterations:

MyBenchmarkSuite run: 100

However, that would only run the benchmark with the default settings in SMark. You can run more complex configurations of your suite using the Harness support, which is a conveninence executor around the runner strategies and the reporter, for example:

SMarkHarness run: { 
	'SMarkHarness'. 
	'SMarkLoops.benchIntLoop' . 
	1 . "The number of iterations"
	1 . "The number of processes"
	5   "The problem size"
	}.

Running built-in benchmarks

For instance to run the built-in benchmarks related to bioinformatics, often used to compare different programming languages, libraries, and algorithms for handling large-scale data processing tasks, these Harness can be evaluated:

SMarkHarness run: { 'SMarkHarness'. 'BenchmarkGameSuite.benchKNucleotide'. 20 . 1 . 2 }.
SMarkHarness run: { 'SMarkHarness'. 'BenchmarkGameSuite.benchFasta'. 25 . 1 . 10 }.
SMarkHarness run: { 'SMarkHarness'. 'BenchmarkGameSuite.benchRegexDNA'. 3 . 1 . 10 }.

The K-Nucleotide benchmark involves counting the occurrences of all k-length substrings (k-mers) in a given DNA sequence. The benchmark will be run 20 times, using 1 process

The FASTA benchmark is composed of 3 sub-benchmarks:

  • The first sub-benchmark writes DNA sequences to a special stream. The nucleotide sequences corresponds to a specific DNA repeated sequence called “ALU“. The benchmark also setup a problem size of 10 to instantiate a custom “repeat” stream, a stream for which its “end” is configured to a limit number and it automatically restarts its position when this end is reached as result of receiving #next. This stream is set to 20 repetitions as limit (2 * problemSize). Finally, the fasta is configured with a line length to write 60 nucleotide positions (columns) before a new line.
  • The second sub-benchmark is configured to 30 repetitions (3 * problemSize) as limit and performs additional calculations instead of just writing the sequence. It iterates another custom repeat stream (a random stream, which uses a naïve linear congruential generator to calculate a random number for each #next message it receives along with percentages – the cumulative probabilities to select each nucleotide) adding and storing the percentage of each ambiguity code (codes used in molecular biology to represent positions in a DNA or protein sequence where the exact nucleotide or amino acid is not known with certainty) in a sequence to provide information about the composition of the sequence in terms of ambiguous and non-ambiguous positions.
  • The third and last sub-benchmark is configured to 50 repetitions as limit but includes preconfigured frequencies instead of performing the ambiguity codes additions for each code.

Finally, the benchRegexDNA measures the performance of regular expression-based DNA sequence analysis on a given DNA sequence, including pattern matching and substitution operations for sequences with “degenerate” codes.

Running from command-line

To run the benchmark harness from CLI:

./pharo -headless Pharo.image --no-default-preferences eval "SMarkHarness run: { 'SMarkHarness'. 'BenchmarkGameSuite.benchFasta'. 1 . 1 . 25000000 }."

And the default output (the console) should look like:

Runner Configuration:
  iterations: 1
  processes: 1
  problem size: 25000000
Report for: BenchmarkGameSuite
Benchmark Fasta
Fasta total: iterations=1 runtime: 14508ms

Some benchmarks are already pre-configured with convenience accessors and default values for the benchmarks game, so it is easier to run them, however, by default, they will not use the SMark reporting support and thus runtime results are not written to the output. For example the FASTA benchmark which expects a 10 kb output file can be run as:

./pharo -headless Pharo.image --no-default-preferences eval "BGFasta fasta"

which output the FASTA format (truncated here):

'>ONE Homo sapiens alu
GGCCGGGCGCGGTGGCTCACGCCTGTAATCCCAGCACTTTGGGAGGCCGAGGCGGGCGGA
TCACCTGAGGTCAGGAGTTCGAGACCAGCCTGGCCAACATGGTGAAACCCCGTCTCTACT
AAAAATACAAAAATTAGCCGGGCGTGGTGGCGCGCGCCTGTAATCCCAGCTACTCGGGAG
GCTGAGGCAGGAGAATCGCTTGAACCCGGGAGGCGGAGGTTGCAGTGAGCCGAGATCGCG
CCACTGCACTCCAGCCTGGGCGACAGAGCGAGACTCCGTCTCAAAAAGGCCGGGCGCGGT
... 

Parametrized benchmarks

If you have complex combinations of benchmarks, for example combining multiple data source with multiple readers and reading strategies, you can write your own benchmarkParameters method in your suite class, to access the built-in support for parametrized test matrix.

To see example code where this is used, check this implementation of JSON benchmarking using NeoJSON and SMark. You can evaluate the benchmark from Pharo with the following expression:

JSONSMarkSuite new benchReadJSON.

In this case, on a new execution, the benchmarks are executed by expanding the matrix of parameters. One important note is that results are collected after a warm up. This means that there will be some executions in what is called the “steady state”, a state where the JIT compiler already produced the code and represents a representative execution.

You can check a benchmark has started looking at the terminal, where output should be something reflecting your parameters:

Runner Configuration:
  iterations: 1
  processes: 1
  problem size: 1

Customize reporting

If you want to send the benchmark output to another destination instead of the console, you can subclass ReBenchReporter with your own reporter class:

ReBenchReporter subclass: #MyBenchmarkReporter
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'MyProject'

and override the method #reportResult:for:of: where you have access to an output stream and an Array of results:

reportResult: aResultsArray for: aCriterion of: benchmark
	"Report duration"
	aResultsArray size < 2 ifTrue: [
		aResultsArray average printOn: stream.
		stream << 'ms'; << $, ].
	"Report iterations count"	
	aResultsArray size printOn: stream.
	stream << $,.
	"Report image name"
	Smalltalk image version printOn: stream.
	stream << $,.
	"Report VM name"
	Smalltalk vm interpreterClass substrings first printOn: stream.
	stream flush.

If this is your case, the harness also should be linked to your reporter:

ReBenchHarness subclass: #MyBenchmarkHarness
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'MyProject'

and the defaultReporter method in class side should answer your reporter class. Also the method defaultOutputDestination should be overriden to configure the output:

defaultOutputDestination
	"Answer a <Stream> used to output receiver's contents"

	^ self defaultOutputFilename asFileReference writeStream

Conclusion

In conclusion, this articule has provided an overview of the SMark benchmark for Pharo. Continued exploration and utilization of SMark will lead to the creation of high-performance software in Pharo, ultimately benefiting the broader community. So, go forth, experiment, and strive for excellence in your Pharo projects with the insights gained from this tutorial.

Happy benchmarking!

Connecting the Dots: Class Names in Pharo Comments Revealed

In this article, I share the outcomes of our experiment aimed at identifying class names, that are references to classes that exist in Pharo environment, within comments in all Pharo classes and methods. The experiment was conducted on a Pharo 11 image, and the identification process relied on regular expressions to detect UpperCamelCases tokens. These tokens were expected to conform to Pharo’s naming conventions for class names. Once they were detected, the next step involved identifying the corresponding class definition for each one of them.

Analysis over class comments

I initiated the analysis on the comments within classes. The environment comprises a total of 9816 classes. Out of these, 8928 classes include comments that do not start with ‘Please comment me’, which is the beginning of default Pharo comments. This indicates that developers have modified 90 % of these comments to add their custom ones.

Using the following regular expression '\b[A-Z][a-zA-Z\d]+\b' asRegex in Pharo, I applied a search over all classes that have custom comments to detect all UpperCamelCase tokens like OrderedCollection. In total I found 3371 classes that have in their comments tokens matching this pattern, moreover, I found in total 7505 references to classes in comments, with duplicates removed in each comment. In other words, if same token is found multiple times in the same comment, it will be counted once to enhance the searching performance over classes in the system.

Having those results, I decided to choose 20 classes with the largest number of references in their comments, and I checked the recall and precision ratios. Below table summarizes 5 classes, to avoid overcrowding the blog post with all the 20 classes:

Class NameRecall ratioPrecision ratio
EncoderForSistaV110.72
IRBytecodeGenerator10.86
ClyNavigationEnvironment10.86
RSLocation10.25
SettingBrowser10.73
Class names in classes comments

On one hand, Recall ratio, also known as the true positive rate, measures the ability to accurately identify all true positive instances. It is high when the number of missing results (False Negatives) is high. On the other hand, Precision ratio focuses on the ability to make accurate positive predictions and avoid including irrelevant matches (false positives). It is high when the number of incorrect matches detected is high.

In the table provided above, the recall ratio for all classes is 1, indicating that the regular expression (regEx) successfully matched all instances of UpperCamelCases in the comments. The only potential scenario where results might be missed is if class names are misspelled or if they are mentioned without adhering to the specified naming convention. As for the precision ratio, results were not as good as the recall ratio because certain keywords, intended for other purposes, were incorrectly identified as classes, leading to more false positives in the classification. Some of them are used in comments to start a sentence (ex: Set the variable …) or a comment block (ex: “` Smalltalk “`).

Analysis over methods comments

Following the execution of the search algorithm within the environment 4,062 classes were found to contain 30,340 commented methods, with a total of 40,422 comments. Upon closer examination, it was determined that 7.5% of these commented methods belong to 368 classes and contain 3,270 references to existing classes in environment.

Having those results, I decided to choose 20 classes with the largest number of references in their methods comments, and I checked the recall and precision ratios. Below table summarizes 5 classes, to avoid overcrowding the blog post with all the 20 classes:

Class NameNumber of commented
methods with references
Recall ratioPrecision ratio
RSLineBuilder1210.94
RSNormalizer710.82
RSLabel910.86
Color2610.95
LargeInteger1711
Class names in methods comments
Boosting performance

Based on the previous research, I discovered that the accuracy of the results could be significantly improved by excluding certain tokens, such as Smalltalk, Set, True, False, Key, etc., which are known to produce false positive results. Additionally, enhancing the research algorithm by excluding code blocks from comments (i.e., everything between “` Smalltalk “` ), would not only optimize the search speed but also ensure that only the intended class names in comments are returned, leading to an overall improvement in precision ratio.

Our research has primarily concentrated on class names rather than method names. However, searching for method references in comments could prove to be more challenging due to two main reasons. Firstly, Pharo’s method naming convention follows camelCase, which increases the possibility of encountering false positive results when attempting to identify them within comments. This occurs because many tokens used in sentences to describe specific functionalities may coincide with method names, leading to potential confusion. Secondly, accurately linking tokens to their corresponding method definitions could also present difficulties, given that the same method might be defined in multiple classes. This situation adds complexity to the process of association and retrieval.

To address the problem, my initial idea involved utilizing AI to identify relevant tokens linked to classes and methods. I discussed this idea with AI experts, but they discouraged its usage for this particular project. They explained that a considerable amount of AI training data, tens of thousands of records, would be necessary to enable AI to accurately detect tokens in less than 10,000 methods and classes. Despite this challenge, the precision and recall ratios were already considered high, and implementing the previously suggested enhancements could further improve them.

Why we need this ?

Following this experiment, I believe that there are reasonable points of why we might need this:

  1. Enhancing Code Documentation: Programs are commented to help other developers understand the purpose and the usage of every code. When class names are clearly mentioned in comments, they provide additional context about the classes and their relationships, making it easier for developers to understand the codebase.
  2. Navigation: In Pharo, every class name that is enclosed between back-ticks like `OrderedCollection` within a comment, becomes clickable in view mode, thus helping the developer navigating to the class definition. However, during our search, we noticed that many classes are not marked in this manner. So why not benefiting from this search and enclosing every token we found in comments that has a valid link to an existing class ?
  3. Maintenance: Also during this search I found some tokens that conform to the naming convention of classes in Pharo, but actually the classes they refer to, do not exist anymore or are deprecated. I guess, this could be solved by enhancing the functionality of renaming a class in Pharo, to apply changes not only over references to these classes in source code, but also in comments, only for those enclosed between back-ticks. However, to be sure this could be done, a previous refactoring should be applied over comments to enclose class names correctly.

Takuzu, a new puzzle game using Bloc

Today I present to you Takuzu, a little puzzle game available for Pharo.

Takuzu is game I proposed to implement during my internship where I’m discovering Bloc, the low-level graphic framework for Pharo 11. The goal of this internship is to create little games with some basic UI using Bloc and after “finishing” the first MineSweeper project (you can find here: https://github.com/Enzo-Demeulenaere/MineSweeper), I started working on this new project.

The name Takuzu is the original Japanese name for this puzzle that is also commonly called ‘Binary Sudoku’ as you fill a grid with 0s and 1s following only 3 rules :
> You can’t have more than 2 cells of the same value aligned.
> There must be as many 0 as 1 on each row and column.
> Rows and columns must be all different one to another.

I first discovered this game around last year when searching for mobile puzzle games when I saw ‘0h h1’ on the Google Play Store (but you can also play it here: https://0hh1.com) and I immediately loved the logic behind it and wondered how could I implement it, what are the algorithms that seem simple but yet hide some complex thinking.

For the UI, I decided to entirely follow the look of 0hh1 with simple red and blue cells on a dark gray background, nothing too creative. Then I had to use Bloc to draw the UI while also using Toplo to have a menu with some widgets and this I what I’ve got so far:

You can discover this project by executing this snippet in Pharo 11:

Metacello new
baseline:'Takuzu';
repository: 'github://Enzo-Demeulenaere/Takuzu:main';
load: 'core'


and launch the application by executing : Takuzu openWithMenuBar

When in the main menu, you can try some pre-loaded levels from size 4×4 to 12×12 in the ‘Levels’ menu or choose the ‘Random’ menu to play with some randomly generated 4×4 and 6×6 levels (but the grids aren’t so good as my generation algorithm isn’t perfect at all)

You can also find all the informations about the project on its GitHub page: https://github.com/Enzo-Demeulenaere/Takuzu

Feel free to give me any feedback about this game and enjoy discovering Takuzu !

Enzo Demeulenaere

Undeclared Variable Reparation, An Epic Journey In a Compiler – Part III

Part III –
More fun with OCUndeclaredVariableWarning

Foo is a nice class, but could it become nicer if we
added an instance variable?

Let us click on the Foo tab of the Calypso window. We
get some code that declares the class.

Object subclass: #Foo
    instanceVariableNames: ''
    classVariableNames: ''
    package: ''

It is a legal Pharo expression that creates (or updates) a subclass
(named Foo) of the Object class (that is the
almost root of the class hierarchy). Creating or updating the
class (accept with Ctrl-S) simply evaluates the expression.
This is neat.

Unrelated note: there is a small check box Fluid in the
bottom right corner that switches to the modern fluid class
syntax.

Object << #Foo
    slots: {};
    package: ''

It is just a different syntax for almost the same stuff. It is still
neat, although a little less neat because while the syntax is better, it
is no more a sufficient expression to declare or update classes.

The thing is that to evaluate some random source code, we need to
compile it first. The evaluation of the class definition is done by
either
ClySystemEnvironment>>#defineNewClassFrom:notifying:startingFrom:
or
ClySystemEnvironment>>#defineNewFluidClassOrTraitFrom:notifying:startingFrom:
according to the fluid check box.

Both methods are really similar (and could be factorized), except
that one warned the developer in a comment about for now, a super
ugly patch
, so we shall look at the other one.

But let us play with the compiler a little without looking at the
code yet.

Object subclass: #Foo
    instanceVariableNames: +''
    classVariableNames: ''
    package: ''

I added a syntax error since + is a binary operator and
the left operand is missing. We evaluate (accept with Ctrl-S), and we
get the following.

class-err1

Object subclass: #Foo
    instanceVariableNames:  Variable or expression expected ->+''
    classVariableNames: ''
    package: ''

The -> way to present syntax error is very old school
(and I hate it) but that is not the point here. The point is that the
compilation process behaved in a sane and expected way:

  • OpalCompiler>>#compile is somewhat executed (cf
    the previous section for the content of the method);
  • it tries to parse, but a syntax error is detected, so an exception
    SyntaxErrorNotification is signaled;
  • OpalCompiler>>#compile catch the exception;
  • notify:at:in: is sent to the requestor (that injects
    the error message in the source code);
  • the failBlock is executed that terminates the method
    ClySystemEnvironment>>#defineNewClassFrom:notifying:startingFrom:.

Let us try with something else:

Object subclass: #Foo
    instanceVariableNames: baz
    classVariableNames: ''
    package: ''

The source code is an expression, and in expressions we can use
variables. Except that here baz is an undeclared variable,
so what happens when we evaluate?

The code is updated to:

class-err2

Object  Variable or expression expected ->subclass: #Foo
    instanceVariableNames: baz
    classVariableNames: ''
    package: ''

Wow. This is bad, and wrong, and bad again.

  • An error message is baldy placed and wrong and unrelated to
    baz.
  • There is no menu asking us what to do with the undeclared variable
    baz like we saw inside a method.
  • Since I work with a small screen, I also noticed an ephemeral
    notification popup box in the bottom left corner of the screen
    (poetically called a growl in Morphic) that stated the truthful
    information “Undeclared Variable in Class Definition”. Such
    growls are usually displayed by invoking the method
    Object>>#inform:.

ClySystemEnvironment>>#defineNewClassFrom:notifying:startingFrom:

Here is the code source of the method:

defineNewClassFrom: newClassDefinitionString notifying: aController startingFrom: oldClass

    "Precondition: newClassDefinitionString is not a fluid class"

    | newClass |
    [
    newClass := (self classCompilerFor: oldClass)
                    source: newClassDefinitionString;
                    requestor: aController;
                    failBlock: [ ^ nil ];
                    logged: true;
                    evaluate ]
        on: OCUndeclaredVariableWarning
        do: [ :ex | "we are only interested in class definitions"
            ex compilationContext noPattern ifFalse: [ ex pass ].
            "Undeclared Vars should not lead to the standard dialog to define them but instead should not accept"
            self inform: 'Undeclared Variable in Class Definition'.
            ^ nil ].

    ^ newClass isBehavior
          ifTrue: [ newClass ]
          ifFalse: [ nil ]

I won’t dive into all the details of this one. What is interesting is
the on: OCUndeclaredVariableWarning do: part that
intercepts the notification, thus preventing it from reaching the end of
the call stack, thus preventing it from executing its default action,
thus preventing it from displaying a menu, thus preventing the user to
repair the code having a semantic error.

Here we can see how it is possible to intercept the default error
reparation mechanism in case of undeclared variables in a specific
context where a temporary or an instance variable does not make much
sense.

What behavior do we get instead?

  • ex compilationContext noPattern ifFalse: [ ex pass ].
    if noPattern is false (double negation isn’t not bad) then
    process the notification anyway. Except that, here,
    noPattern isn’t unlikely to not be not true (nested
    negations are annoying, aren’t they?) because it is what distinguishes
    the compilation of an expression from the compilation of a method
    definition: a method definition starts with a name (and potential
    arguments) that is called the method pattern. But the first
    statement of the OpalCompiler>>#evaluate method that
    is called it to override noPattern with true, because one
    can only evaluate expressions, not method definitions.
  • self inform: 'Undeclared Variable in Class Definition'
    is responsible for the growl we get.

    You know what I hate? The -> error message
    insertions. You know what I hate more? Inconsistencies. Here, the source
    code error is reported as a (missable) popup with poor information,
    whereas all other errors are reported with ->.

Nevertheless, is the design legitimate? I’m not a fan of exceptions,
they make code comprehension harder and, in my humble opinion, should be
used with great reserve. Here there is also some breach of
encapsulation. But this is debatable. What is less debatable is that the
whole reparation of undeclared variables is bypassed completely,
including some legitimate needs.

For instance, we get no help if we try to evaluate
(Ctrl-S accept) Objectt subclass: #Bar, which
contains too much t in the identifier of the superclass
Object. All we get is an unhelpful growl and the same
wrongful -> error message insertion. As a matter of
fact, as Pharo users, we can bypass the accept (Ctrl-S) behavior and
just evaluate the code in place by selecting all the text (Ctrl-A) then
Doing It (Ctrl-D). The DoIt simply evaluates the
selected source code without (too much) hacking. So there is no
intercepting OCUndeclaredVariableWarning for instance, and
we get the menu “Unknown variable: Objectt please correct, or
cancel
” that presents Object (with a correct amount of
t) in the list of choice. We can select it and we get a
successful class definition and a new available class
Bar.

But why does the code signal a missable popup instead of a classic
text error insertion? Maybe because the
OCUndeclaredVariableWarning that is caught might not come
from the class definition syntax. When a random string is evaluated, it
can do a lot of things, like signaling exceptions. Unfortunately, a
broad error handling mechanism like exceptions has no way to distinguish
exceptions that come from the analysis of the code (syntactic and
semantic error) from the ones that come from the proper evaluation.

And that happens frequently. If you remove an instance variable
(attribute) from a class definition, then accept the new definition, the
system will recompile all the methods of this class. Methods that use
the removed instance variable will also be compiled and signal
OCUndeclaredVariableWarning. That exception will be caught
and growl “Undeclared Variable in Class Definition”. Note that
the message is misleading since the undeclared variable is not in the
class definition. So maybe it was not the original intention of the
growl and was just a random inconsistency.

Let us discuss the last statement of the method. It is a sanity
check. Because the initial class definition expression could have been
heavily edited by the programmer and replaced by anything else, the
method checks that the final result of the evaluation is a class-like
object. Otherwise, nil is returned.

ClyClassDefinitionEditorToolMorph>>#applyChangesAsClassDefinition

OK, we have an explanation for the absence of the menu, and an
explanation for the presence of the growl, but noting here is related to
the wrong -> syntax error insertion. Where does this one
come from?

First, there is no syntax error in the expression (it is a lie!). The
compiler manages to signal an OCUndeclaredVariableWarning
notification (the growl is the proof!) launched by the
OCASTSemanticAnalyzer, meaning that the parser can produce
an AST and not find any syntax error.

So, what is the deal?

  • We are at
    ClySystemEnvironment>>#defineNewClassFrom:notifying:startingFrom:,
  • that is called by
    ClySystemEnvironment>>#compileANewClassFrom:notifying:startingFrom:,
  • that is called by
    ClyFullBrowserMorph>>#compileANewClassFrom:notifying:startingFrom:,
  • that is called by
    ClyClassDefinitionEditorToolMorph>>#applyChangesAsClassDefinition,
  • that goes like this:
applyChangesAsClassDefinition

    | newClass oldText |
    oldText := self pendingText copy.
    newClass := browser
                    compileANewClassFrom: self pendingText asString
                    notifying: textMorph
                    startingFrom: editingClass.

    "This was indeed a class, however, there is a syntax error somewhere"
    textMorph text = oldText ifFalse: [ ^ true ].

    newClass ifNil: [ ^ false ].

    editingClass == newClass ifFalse: [ self removeFromBrowser ].
    browser selectClass: newClass.
    ^ true

We see the invocation of the compilation (the
newClass := browser compileANewClassFrom: thing). Because
it failed, newClass is nil (instead of a class).

What follows is interesting:
textMorph text = oldText ifFalse: [ ^ true ]. This states
that if the text in the code editor was changed during the compilation,
then there was an error. Interesting and sooo wrooong on sooo many
leveeels:

  • Detecting syntax error should not be done thanks to string
    comparison.
  • The method should not assume that error reporting to the user
    changed the source code (even if it is the old school way and is the
    active tradition of the present and previous millennium). For instance,
    instead of an ugly -> something might have preferred to
    display a growl (even if inconsistencies are bad, and I hate them, here
    the culprit is not the inconsistency).
  • Code change might be related to some code reparation that fixed an
    error, so exactly the opposite of an error.

ClyClassDefinitionEditorToolMorph>>#applyChanges

But wait, there is more, because newClass is nil, the
method returns false to its caller, that is
ClyClassDefinitionEditorToolMorph>>#applyChanges and
is defined by:

applyChanges

    | text |
    text := self pendingText copy.
    ^ self applyChangesAsClassDefinition or: [
          self pendingText: text.
          self applyChangesAsMethodDefinition ]

What the heck is that? I do not even understand what is the objective
of this thing! The defining class is
ClyClassDefinitionEditorToolMorph that is the widget whose
sole job as a text editor is to define new classes and to update
existing classes. And the Pharo way to do that is by evaluating
expressions that define or update classes. It seems to be an easy job
that even an unaware DoIt action can manage
successfully.

So what is this insane method doing:

  • saves the source code’s content (to avoid code change in the editor
    due to syntax error or code reparation);
  • tries to evaluate as a class definition (an expression);
  • if the result is false (and it is, in our case), then
    tries to compile the original pristine source code as a method
    definition.

OK.

Let’s just do that. Remember that the class definition we try to
process is:

Object subclass: #Foo
    instanceVariableNames: baz
    classVariableNames: ''
    package: ''

Let us try to parse this source code as a method definition instead
of as an expression.

  • A method starts with a method pattern that can be many
    things, but for simple unary methods, they are simple and plain
    identifiers. Do we have a simple and plain identifier? Yes, the token
    Object (RBIdentifierToken).
  • A method then have a body, with statements, that usually start with
    an expression. What follows is the token subclass:
    (RBKeywordToken) which is not the beginning or any correct
    expression. But the parser wants an expression right now! So it
    reports the error
    Variable or expression expected ->subclass:.

It’s the beauty of computer science. Whatever insane behavior we
might witness, there is always a rational explanation.

A Final Experiment

Can we bypass the bypass of OCUndeclaredVariableWarning
with the undefined variable baz? Let’s try the
DoIt way, it made wonder with the superfluous t of
Objectt some sections ago.

  • Select the full text (Ctrl-A);
  • Do It (Ctrl-D);
  • The menu “Unknown variable: baz please correct, or cancel”
    appears and proposes: a new temporary variable, a new instance variable
    or to cancel;
  • Chose the “temporary variable”;
  • A debugger window appears: “Instance of ClyTextEditingMode did
    not understand #textMorph
    ”. What?
  • The error is caused by the line
    theTextString := self requestor textMorph editor paragraph text.
    of
    OCUndeclaredVariableWarning>>#declareTempAndPaste:.
    We discussed this (rather ugly) line in a previous section, stating that
    it is fragile. What a coincidence

Conclusion and Perspective

During this exploration of the
OCUndeclaredVariableWarning we discovered a lot of classes
and methods with a very variable quality of code and design. Obviously,
the present article focuses on the discussable parts that could be
improved, because it forces us to understand why things are bad, and how
they could be improved. It is also fun to see concrete effects of how
things can go bad when software design is not as tidy as it should
be.

Pharo is a wonderful dynamically typed programming language with
great features, abstractions and powerful semantics. And with great
power comes great responsibility.

An example could be the requestor thing. Adding a dependency between
UI and the compiler work is enough to raise some eyebrows (independently
of the programming language or its paradigm). But in Pharo this
dependency appears as an unwritten API (orality-based API?) with some
inconsistent or fragile hacks: notify:at:in:,
Object>>#bindingOf:,
requestor respondsTo: #interactive,
requestor textMorph,
requestor class name = #RubEditingArea, etc. This also
causes subtitle breakages when someone tries to fix things, breakages
that are often hard to catch because, for instance, they could be only
related to untested or rare UI interactions.

A lot of change is currently ongoing for Pharo 12 on the compiler. We
are in the early part of its development cycle, so it’s the best moment
to try large and disruptive hacks. At the time of publishing, most of
the design issues discussed here are already fixed. But they represent a
specific use case, and a lot of work is still needed. The full
meta-issue is available at
https://github.com/pharo-project/pharo/issues/12883.

Undeclared Variable Reparation, An Epic Journey In a Compiler – Part II

Part II – The Return Journey

undeclvar

Welcome to the next step in the compiler journey.

As a simple recap, we were compiling baz := 42 in a
method bar, except that baz is not declared.
We are currently in
OCSemanticWarning>>#defaultAction, the default action
of an uncaught Notification, that is ready to open a graphical menu by
calling openMenuIn:.

OCUndeclaredVariableWarning>>#openMenuIn:

The method is long; let us review it in small pieces.

openMenuIn: aBlock
    | alternatives labels actions lines caption choice name interval requestor |

A bunch of temporary variables.

    "Turn off suggestions when in RubSmalltalkCommentMode
    This is a workaround, the plan is to not do this as part of the exception"
    requestor := compilationContext requestor.
    ((requestor class name = #RubEditingArea) and: [
        requestor editingMode class name = #RubSmalltalkCommentMode])
                    ifTrue: [ ^UndeclaredVariable named: node name ].

These are some type checks. Type checks are usually bad. Those are
bad.

They prevent the menu thing if the requestor is a
RubEditingArea in a RubSmalltalkCommentMode
“mode”. Where RubSmalltalkCommentMode is used?

  • By ClyRichTextClassCommentEditorToolMorph, which seems
    never used in the system (dead class?)
  • By RubEditingArea>>#beForSmalltalkComment, that
    is called only by FileList, a basic file explorer, but it’s
    not clear when or why the compiler is called by the file explorer, nor
    why a “workaround” is needed here (quite deep) in the compiler
    especially since it’s the only workaround of this type in the whole
    source code for RubEditingArea or
    RubSmalltalkCommentMode.

It could be just dead code, so less problematic: a dead workaround is
a less technical dept than a live one. It also illustrates why type
checks can be bad; it reverses the responsibility:
RubEditingArea and RubSmalltalkCommentMode
here are not involved at all in the workaround, so code evolution
related to one of these two classes might likely miss the present
hack.

Moreover, such a workaround is fragile. The compiler should not care
about specific clients, and especially not care about their names, and
should behave equitably. E.g. imagine renaming classes or using
subclasses of the blacklisted ones, they could likely pass the check and
cause really subtle bugs.

Anyway, let us continue with the method:

    interval := node sourceInterval.
    name := node name.
    alternatives := self possibleVariablesFor: name.
    labels := OrderedCollection new.
    actions := OrderedCollection new.
    lines := OrderedCollection new.

All those are the initialization of the temporary variables.

OCUndeclaredVariableWarning>>#possibleVariablesFor:
provides a list of existing names usable as a replacement (sorted from
the best match to the worst match). See
String>>#correctAgainst:continuedFrom: for the
details and the scoring system.

    name first isLowercase
        ifTrue: [
            labels add: 'Declare new temporary variable'.
            actions add: [ self declareTempAndPaste: name ].
            labels add: 'Declare new instance variable'.
            actions add: [ self declareInstVar: name ] ]

The two first items of the menu. If the name looks like a temporary
or an instance variable, because it starts with a lowercase letter, then
maybe the programmer wants a new temporary or instance variable?

Note that there are two parallel lists, one of the labels (shown in
the menu) and the other of actions (here blocks), that is evaluated if
the user chose the corresponding label.

In the scenario, the option
Declare new temporary variable is selected, so
self declareTempAndPaste: name is eventually called. We
detail it in the next section. For now, we continue to read the
method.

        ifFalse: [
            labels add: 'Leave variable undeclared'.
            actions add: [ self declareUndefined ].
            lines add: labels size.
            labels add: 'Define new class'.
            actions
                add: [
                    [ self defineClass: name ]
                        on: Abort
                        do: [ self openMenuIn: aBlock ] ].
            labels add: 'Declare new global'.
            actions add: [ self declareGlobal ].
            compilationContext requestor isScripting ifFalse:
                [labels add: 'Declare new class variable'.
                actions add: [ self declareClassVar ]].
            labels add: 'Define new trait'.
            actions
                add: [
                    [ self defineTrait: name ]
                        on: Abort
                        do: [ self openMenuIn: aBlock ] ] ].

For names that start with an uppercase, they look like global
variables, and that includes all the named classes, so the proposed
items in the menu are different. A first curiosity, there is the choice
to “leave variable undeclared” that is absent in the previous code
snippet. Another curiosity, defining a new class (or a new trait) opens
a new window, but if, for some reason, the entity creation fails or is
canceled, then a recursive call is used to open the same menu again.

    lines add: labels size.
    alternatives
        do: [ :each |
            labels add: each.
            actions
                add: [
                    ^self substituteVariable: each atInterval: interval ] ].
    lines add: labels size.
    labels add: 'Cancel'.
    caption := 'Unknown variable: ' , name , ' please correct, or cancel:'.

We have the addition of the possible variables (computed at the
beginning of the method), a cancel item, and the window title. The next
last two lines are the fun ones.

    choice := aBlock value: labels value: lines value: caption.
    ^choice ifNotNil: [ self resume: (actions at: choice ifAbsent: [ compilationContext failBlock value ]) value ]
  • aBlock is the parameter of the method, it was more
    than 50 lines ago, so we almost forgot about it. It is always
    [:labels :lines :caption | UIManager default chooseFrom: labels lines: lines title: caption]
    that just calls the UI and returns the index number of the selected item
    starting at 1 (or 0 if the cancel button is used).
  • The selector value:value:value: is used to evaluate
    the block with 3 supplied arguments (it is a Pharo thing, do not
    judge).
  • ^choice ifNotNil: ... returns nil if the choice is
    nil (unlikely according to the API of chooseFrom, but
    better safe than sorry). In the scenario, the first choice is selected
    (declare new temporary variable). Therefore choice is 1, which is not
    nil, so we look at the ifNotNil: part.
  • self resume: cause the signal to finish
    is execution with the given value. Hopefully, a Variable
    object to bind to baz (look back at the section
    OCASTSemanticAnalyzer>>#undeclaredVariable: if you
    need to see the original signal method invocation). Here,
    the call to resume feels superfluous as the result of the
    current method is used as the result of defaultAction that
    is used as the value of the automatic resume call performed
    on Notification objects (see
    UndefinedObject>>#handleSignal:).
  • actions at: choice return the action (the block)
    associated to the corresponding choice number. Ordered collections in
    Pharo are 1-based; therefore 1 is the first block action. Here, the
    block [ self declareTempAndPaste: name ].
  • ifAbsent: is for what to do when there is no
    corresponding action for the given choice. This happens when the user
    chooses the cancel button (no action for 0) or chooses the cancel item
    (no action for 3 in our scenario).
  • compilationContext failBlock value is therefore
    executed on a “cancel”. It evaluates the failBlock that, in the
    scenario, comes from the ClassDescription>>#compile
    method and contains [ ^ nil ] (a non-local return).
    Evaluating this failBlock cause the unwinding of many methods in the
    call stack (something around 30 or 40 frames) and the return of the
    ClassDescription>>#compile method with nil.

    Note that there is a potential weakness here if the failBlock does
    not perform a non-local return, then the result of the block evaluation
    is used as the return of openMenuIn: and eventually used as
    a Variable object to bind baz to. Callers of
    the compiler might forget to do that and just provide
    [nil], for instance (without a ^).

  • value evaluates the action block (since it exists in
    the list), that has the responsibility to provide a
    Variable instance.

OCUndeclaredVariableWarning>>#declareTempAndPaste:

We selected “declare new temporary variable” in the menu, thus
executing this method. We’ll cover this large method (35 lines) piece by
piece.

declareTempAndPaste: name
    | insertion delta theTextString characterBeforeMark tempsMark newMethodNode |

Some temporary variables.

    "Below we are getting the text that is actually seen in the morph.
     This is rather ugly. Maybe there is a better way to do this."
    theTextString := self requestor textMorph editor paragraph text.

Indeed, this is rather ugly. This leads to many
questions:

  • Why is the text (source code) of the method bar
    needed?
  • Why does it assume that the requestor has a textMorph
    method?
  • Why ask for something so deep? Demeter is likely rolling over in its
    grave (it’s a joke on the Law of Demeter. Demeter is not dead
    and is not even a person, it was a project named after the Greek goddess
    of Agriculture).
  • Why? We are still in a (deep) part of the compiler,
    self should have a better way to get the source code
    currently compiled.
    "We parse again the method displayed in the morph.
     The variable methodNode has the first version of the method,
     without temporary declarations. "
    newMethodNode := RBParser parseMethod: theTextString.

Let us take a breath.

We are doing a semantic analysis on an already parsed source code of
a method bar trying to get a variable to bind to
baz. And we parse the full source code again? Don’t we have
it? Just call self node methodNode or something?

The hint might be “without temporary declarations” from the
comment. Does that mean we do not trust the current AST to be genuine?
Why? Maybe the previous interactive code error reparation changed the
current AST? Is this actually true in some possible scenarios? Is this
just leftover code?

Let us just continue… we must continue…

    "We check if there is a declaration of temporary variables"
    tempsMark :=  newMethodNode body  rightBar ifNil: [ self methodNode body start ].

It’s getting warm here, isn’t it?

  • newMethodNode body rightBar gets the position (an
    integer) of the closing | character of the temporary
    variable declaration syntax, or nil if there is no temporary variable
    declared (like in the current scenario) an AST is useful for this task
    since it knows which part of the source code is really a block of
    temporary variable declarations.
  • self methodNode body start is the position (an integer)
    of the beginning of the main body of the method, that position is
    therefore used when there are no declarations of temporary
    variables.
    characterBeforeMark := theTextString at: tempsMark-1 ifAbsent: [$ ].

gets the character before the closing | or before the
main body. The ifAbsent might only occur if the source code
is empty, and the compiler let us progress until here because an empty
method is a syntax error, a method name is minimally needed (the name
(selector) and parameters are called the “method pattern” in Pharo
parlance). But better safe than sorry.

    (theTextString at: tempsMark) = $| ifTrue:  [
        "Paste it before the second vertical bar"
        insertion := name, ' '.

        characterBeforeMark isSeparator ifFalse: [ insertion := ' ', insertion].
        delta := 0.
    ] ifFalse: [

Some temporary variables are declared, and we want to add the new
variable after the last one. The code mainly manages spacing to avoid
concatenating the new variable and a previous one, or injecting
superfluous spaces.

In our scenario, there is no temporary variable (yet), so the
ifFalse: part interests us more.

        "No bars - insert some with CR, tab"
        insertion := '| ' , name , ' |',String cr.
        delta := 2. "the bar and CR"
        characterBeforeMark = Character tab ifTrue: [
            insertion := insertion , String tab.
            delta := delta + 1. "the tab" ]
        ].

Here we prepare the text to insert in the source code and compute a
delta thing, we’ll discuss that later. The code tries to
care about preserving the indentation, if any.

    tempsMark := tempsMark +
        (self substituteWord: insertion
            wordInterval: (tempsMark to: tempsMark-1)
            offset: 0) - delta.

Err… it’s getting cold here, isn’t it?

self substituteWord: insertion wordInterval: (tempsMark to: tempsMark-1) offset: 0
asks to insert the new string in the source code (because the interval
is empty, it is an insertion and not a replacement).

How does
OCUndeclaredVariableWarning>>#substituteWord:wordInterval:offset:
do that? By simply calling #correctFrom:to:with: on the
requestor and doing some math, then doing more math to update
tempsMark.

    " we can not guess at this point where the tempvar should be stored,
    tempvars vs. tempvector therefore -> reparse"
    (ReparseAfterSourceEditing new newSource: self requestor text) signal

And it is the end of the method. I think it’s getting humid here,
isn’t it?

The code is altered. We have no idea what really happened, there is a
new source code in town. The full AST might need to be rebuilt as there
are new potential AST nodes. The semantic analysis might need to be
redone, as the new temporary variable might conflict with other
variables declared further in the code. So at this point, it seems
better to just call it a day and run the compilation again.

It’s the point of the ReparseAfterSourceEditing class
that is a subclass of Notification (we are now experts in
notifications and no afraid no more of them!).

There are still some questions about the behavior of the program and
some of its design decisions:

  • The math thing about offset, delta, and
    tempsMark update is completely unused. Possible leftover of
    previously removed code.
  • Where does the signal on ReparseAfterSourceEditing
    go?
  • Why the new source code should be passed around in the notification?
    We did already update it in the requester.
  • Why does the ugly (indeed)
    self requestor textMorph editor paragraph text at the
    beginning of the method exists, since apparently
    self requestor text give the same damn source code (while
    not ideal, it is still better).
  • What happens when the notification is resumed? The point of a
    notification is to be resumable. Here it clearly appears that such an
    endeavor is not supposed to happen.
  • Why so much coupling?
  • Why so little cohesion?
  • And more specifically, why it is the job of
    OCUndeclaredVariableWarning to perform this menu and string
    based code reparation and hijack the requestor as if there were friends
    in some abusive relationship? Shouldn’t a notification be just a means
    of sending some kind of signal to the previous method in the call
    stack?

There are a lot of symptoms of schizophrenia in the responsibilities
here.

OpalCompiler>>#parse

Where does ReparseAfterSourceEditing go? I
(intentionally) skipped some steps between
OpalCompiler>>#compile and
OCASTSemanticAnalyzer>>#undeclaredVariable:.

Here is the source code of
OpalCompiler>>#parse:

parse
    | parser |
    [
        parser := self createParser.
        ast := self semanticScope parseASTBy: parser.

        ast methodNode compilationContext: self compilationContext.
        self callParsePlugins.
        self doSemanticAnalysis ]
    on: ReparseAfterSourceEditing do: [:notification |
            self source: notification newSource.
            notification retry ].

    ^ ast

The job of this method (as explained in a previous section) is to do
the frontend part of the compilation and produce a fully annotated and
analyzed AST of the method so that (virtual) machine code can be
generated. The content of the method is mostly straightforward.

What is interesting is the on:do: method call used for
exception (and thus, notification) handling. When a
ReparseAfterSourceEditing is intercepted, we update the
source code and run the protected block from the beginning again (see
Exception>>#retry).

Some potential exits of this loop are:

  • The source code is good enough and no more
    ReparseAfterSourceEditing are signaled.
  • The source code is bad enough that either the failBlock
    is invoked, or another unrecoverable exception occurs. Remember
    SyntaxErrorNotification in
    OpalCompiler>>#compile, for instance.
  • Someone is tired enough and terminates the process.

The first alternative is what happens in our scenario:

  • A declaration of the temporary variable baz is added to
    the source code.
  • The content editor window is updated to reflect that.
  • The new code source is fully parsed and correctly analyzed and a
    legitimate CompiledMethod is produced.

What a happy ending!