Preface: The reason for this post on GF is that GF version 3.12 was released on 8 August 2025, with the previous version, 3.11, released three years ago, back in 2021-07-25. What’s more, GF 3.12 provides support for Apple Silicon Mac computers (M1, M2, M3, M4).
What is Grammatical Framework (GF)
GF (Ranta, 2011) is both a formal grammar language and a development platform.
With GF we can solve a wide range of language specific needs, such as natural language processing, translation, and create grammar-based software tools. GF provides a CLI and a programming language, which we can
use to build multilingual grammars
⠀⠀⠀⠀⠀Perhaps the biggest exclamation point is, that GF is Rule Based while LLMs like
ChatGPT are Stat Based. An LLM needs to be trained on a lot of data, while with
GF you can litteraly just write a grammar, compile it, and it’ll work.
The trade-off is flexibility, with an LLM you can handle a very wide range of sentences,
while a rule based system like GF can only handle sentences that it can generate from it’s grammars.
⠀⠀⠀⠀⠀What’s neat about GF, is that we can express meaning, like greeting, by creating an abstract grammar, and then decide how to manifest that meaning_, like “Hello”, “Bonjour”, “Nǐ hǎo”, by creating concrete grammars for a range of different languages. Later in this post, we’ll create an example, where we write gramamrs for a pizza ordering system.
Read More: For more on GF, check out the framework’s official website.
🔰 Get started with GF (on an Apple Silicon Mac)
GF is available on pretty much every big platform; Windows, Linux, Intel Mac, and Silicon Mac. For the other platforms, please read the installation instructions.
Now that we have a rough idea of what GF is, let’s get it running on our Silicon Mac. We are going to (1) download the official GF 3.12 binary, (2) install it, (3) write a simple grammar, use the GF CLI to (4) compile the grammars, and finally (5) try to intergrate the generated grammars in a simple Rust application. Let’s get started.
TODO: 🔥 Get the right number of steps listed.
🐱 Step 01: Get the binary
First we need to grab the Grammatical Framework binary.
Go to https://www.github.com/gf-core/releases and downlod the latest release for Apple Silicon Mac (3.12 at this time of writing). The name of the binary should be: gf-3.12-macos-arm.pkg and is about 20 Mbs in size.
🐶 Step 02: Install the binary
Locate the downloaded binary, double-click it, and follow the installer instructions. If everything went well, we now have the GF CLI accessible in our terminal.
Note: When you double click the installation file, you may get a warning stating that “macOS cannot verify the developer of “gf-3.12-macos-intel.pkg”. Are you sure you want to open it?”. Go to your Mac’s System Settings, under ‘Privacy & Security’, click “Open Anyway”.
# In a terminal run:
gf version
* * *
* *
* *
*
*
* * * * * * *
* * *
* * * * * *
* * *
* * *
This is GF version 3.12.0.
Built on darwin/aarch64 with ghc-9.6 at 2025-08-13 12:09:33
Git info: commit fa2826d tag
Flags: interrupt server c-runtime
License: see help -license.
Resource: For more GF commands, see the Appendix at the end of this post👇.
🐭 Step 03: Dialogue Use Case
In order to test that our installed gf CLI works, we’re goign to create a small
grammar and then test it. Everyone loves pizza, so let’s create a Pizza Ordering Grammar.
The scenario: We’ll start by writing an imaginary User Scenario, as a Wizard of Oz-technique, and then use that scenario to source what needs to be in our grammar. So, imagine the following:
⠀⠀⠀⠀⠀"Eve has been coding all day without breaks, and now she’s starving. She decides to order a pizza. Since it’s 2025, she does so, right in her Discord chat window. Using the marvels of The Agentic Web, and the fact that she’s a member of the PizzaHut Discord server, Eve initiates the diagloue below."
# Discord Dialogue
👩Eve: "@PizzaHut, I need your help!"
🍕PizzaHut: "Hi! Welcome to Pizza Hut! What can I do for you?"
👩Eve: "I want to order a pizza."
🍕PizzaHut: "Great! Which pizza did you have in mind?"
👩Eve: "I want to order a Capriciosa."
🍕PizzaHut: "One Capriciosa. Anything else?"
👩Eve: "No, thank you. That's all!"
🍕PizzaHut: "What addres do you want the pizza delivered to?"
👩Eve: "2210, Burbank Street."
🍕PizzaHut: "2210, Burbank Street."
🍕PizzaHut: "The pizza will be delivered in 30 minutes."
👩 "Thank you! Bye!"
There are lots of details that we could add, but let’s keep things simple for now. Our intention is not to create a capable system. Our goal is to test out Grammatical Framework 3.12 on our Apple Silicon Mac. Let’s proceed.
🔬 Step 04: Discourse Analysis
If we break down the semantic elements in the discourse above, we find
that we can extract dialogue categories and speech acts. 👩Eve, in expressing; “Hi! I want to order a pizza.”
performs a Order Initiation dialogue category, and an Expressive/Phatic speech act.
By analyzing the discourse above we can extract which dialogue categories and speech acts that our grammar needs to handle:
| Speaker | Dialogue | Category | Speech Act |
|---|---|---|---|
| 👩 | “@PizzaHut, I need your help!” | Greeting | Expressive / Phatic |
| 🍕 | “Hi! Welcome to Pizza Hut! What can I do for you?” |
Greeting | Expressive / Phatic |
| 👩 | “I want to order a pizza.” | Order Initiation | Directive |
| 🍕 | “Great! Which pizza did you have in mind?” | Choice | Directive |
| 👩 | “I want to order a Capriciosa.” | Choice | Directive |
| 🍕 | “One Capriciosa. Anything else?” |
Confirmation | Commissive / Directive |
| 👩 | “No, thank you.” | Confirmation | Commissive / Assertive |
| 👩 | “That’s all!” | Order Finalization | Commissive / Assertive |
| 🍕 | “What addres do you want the pizza delivered to?” | Delivery info | Directive |
| 👩 | “2210, Burbank Street.” | Delivery info | Assertive |
| 🍕 | “2210, Burbank Street.” | Delivery info | Assertive |
| 🍕 | “The pizza will be delivered in 30 minutes.” | Delivery info | Assertive |
| 👩 | “Thank you! Bye!” | Farewell | Expressive / Phatic |
Read More: How Speech Act Theory can be used to analyze intentions in sentences.
🖊️ Step 05: Write Grammars
Now that we have a User Story, analysed for it’s Dialogue Categories, and their Speech Acts, let’s move on and
create the grammars needed for GF to be able to handle this discourse.
⠀⠀⠀⠀⠀ When we say handle, we mean be able to parse strings into Abstract Syntax Trees (representing meaning) and vice versa - linearize ASTs into strings in a natural language. Again, since this example is only for demonstration, we’ll keep the grammar small.
⠀⠀⠀⠀⠀ We’ll note, however, that crafting performant production grade grammars is a very delicate and complicated task, which requires expert level compentence - far from what
we’re doing here.
📘 Abstract Grammar File: PizzaOrder.gf
abstract PizzaOrder = {
cat
Conversation ;
PizzeriaGreeting ;
CustomerGreeting ;
Order ;
Pizza ;
DeliveryDetails ;
Quantity ;
fun
Dialogue : PizzeriaGreeting -> CustomerGreeting -> Order -> DeliveryDetails -> Conversation ;
-- Greetings
HelloPizzeria : PizzeriaGreeting ;
HelloCustomer : CustomerGreeting ;
-- Quantities
One : Quantity ;
Two : Quantity ;
Three : Quantity ;
-- Orders
SinglePizza : Quantity -> Pizza -> Order ;
MultipleOrders : Order -> Order -> Order ;
-- Variants of asking
WouldLike : Order -> Order ;
CanIHave : Order -> Order ;
IllTake : Order -> Order ;
-- Pizza types
Margherita : Pizza ;
Pepperoni : Pizza ;
FourCheese : Pizza ;
-- Delivery details
DeliverTo : Str -> DeliveryDetails ;
PickUp : DeliveryDetails ;
}
📗 Concrete Grammar File: PizzaOrderEng.gf 🏴
concrete PizzaOrderEng of PizzaOrder = open SyntaxEng, LexiconEng, ParadigmsEng in {
lincat
Conversation, PizzeriaGreeting, CustomerGreeting, Order, Pizza, DeliveryDetails = Utt ;
Quantity = Det ;
lin
Dialogue pg cg o dd =
mkUtt (mkText [pg, cg, o, dd]) ;
-- Greetings
HelloPizzeria = mkUtt (mkText "Hello, this is Luigi's Pizzeria. How can I help you today?") ;
HelloCustomer = mkUtt (mkText "Hello, good evening!") ;
-- Quantities
One = a_Det ;
Two = mkDet (mkNumeral "two") ;
Three = mkDet (mkNumeral "three") ;
-- Orders
SinglePizza q p = mkUtt (mkNP q p) ;
MultipleOrders o1 o2 = mkUtt (mkNP and_Conj (fromUtt o1) (fromUtt o2)) ;
-- Variants
WouldLike o = mkUtt (mkCl (mkNP I_Pron) (mkVP (mkV2 "like") (fromUtt o))) ;
CanIHave o = mkUtt (mkQS (mkQCl can_VV (mkNP I_Pron) (mkVP have_V2 (fromUtt o)))) ;
IllTake o = mkUtt (mkCl (mkNP I_Pron) (mkVP (mkV2 "take") (fromUtt o))) ;
-- Pizzas
Margherita = mkN "Margherita pizza" ;
Pepperoni = mkN "Pepperoni pizza" ;
FourCheese = mkN "Four Cheese pizza" ;
-- Delivery
DeliverTo addr = mkUtt (mkText ("Please deliver it to " ++ addr)) ;
PickUp = mkUtt (mkText "I will pick it up myself.") ;
}
📙 Concrete Grammar File: PizzaOrderFre.gf 🇫🇷
concrete PizzaOrderFre of PizzaOrder = open SyntaxFre, LexiconFre, ParadigmsFre in {
lincat
Conversation, PizzeriaGreeting, CustomerGreeting, Order, Pizza, DeliveryDetails = Utt ;
Quantity = Det ;
lin
Dialogue pg cg o dd =
mkUtt (mkText [pg, cg, o, dd]) ;
-- Greetings
HelloPizzeria = mkUtt (mkText "Bonjour, ici la pizzeria Luigi. Que puis-je faire pour vous ?") ;
HelloCustomer = mkUtt (mkText "Bonjour, bonsoir !") ;
-- Quantities
One = un_Det ;
Two = mkDet (mkNumeral "deux") ;
Three = mkDet (mkNumeral "trois") ;
-- Orders
SinglePizza q p = mkUtt (mkNP q p) ;
MultipleOrders o1 o2 = mkUtt (mkNP et_Conj (fromUtt o1) (fromUtt o2)) ;
-- Variants
WouldLike o = mkUtt (mkCl (mkNP I_Pron) (mkVP (mkV2 "vouloir") (fromUtt o))) ;
CanIHave o = mkUtt (mkQS (mkQCl (mkVP (mkV2 "pouvoir") (mkVP (mkV2 "avoir") (fromUtt o)))))) ;
IllTake o = mkUtt (mkCl (mkNP I_Pron) (mkVP (mkV2 "prendre") (fromUtt o))) ;
-- Pizzas
Margherita = mkN "pizza Margherita" ;
Pepperoni = mkN "pizza Pepperoni" ;
FourCheese = mkN "pizza quatre fromages" ;
-- Delivery
DeliverTo addr = mkUtt (mkText ("Merci de livrer à " ++ addr)) ;
PickUp = mkUtt (mkText "Je viendrai la chercher moi-même.") ;
}
🔧 Step 06: Compile Grammars
Before we can actually use the grammars that we have written, we have to compile them using GF.
But what does compiling a grammar really mean?
⠀⠀⠀⠀⠀ One of the main strengths of Grammatical Framework, is its ability to create Abstract Syntax Trees (AST). An AST can be seen as the syntactic representation of a natural language sentence.
We could even, somewhat hand-wavey, think of the AST as the meaning of a sentence.
What is not hand-wavey is the ASTs function as a an interlingua between grammars
of different languages (for example English and French). The AST is what makes it possible for GF to translate
a sentence in one langauge “I want to order a pizza!” into another language “Je veux commander une pizza !”.
⠀⠀⠀⠀⠀ So, let’s use GF to create an AST for the gramars that we wrote earlier, and translate a sentence between English and French:
6.1. Compile the Abstract, English, and French grammars into an Abstract Syntax Tree (AST)
gf PizzaOrder.gf PizzaOrderEng.gf PizzaOrderFre.gf
6.2. Parse an english sentence and generate its AST
parse -lang=Eng "Hello, this is Luigi's Pizzeria.
How can I help you today? Hello, good evening!
I would like a Margherita pizza and a Pepperoni pizza
Please deliver it to 221B Baker Street."
which returns:
Dialogue HelloPizzeria HelloCustomer
(MultiplePizzas Margherita (SinglePizza Pepperoni))
(DeliverTo "221B Baker Street")
6.3. Linearize an AST into French
linearize -lang=Fre Dialogue HelloPizzeria HelloCustomer
(MultiplePizzas Margherita (SinglePizza Pepperoni))
(DeliverTo "221B Baker Street")
which returns:
Bonjour, ici la pizzeria Luigi. Que puis-je faire pour vous ?
Bonjour, bonsoir !
Je voudrais une pizza Margherita et une pizza Pepperoni Merci
de livrer à 221B Baker Street.
Great! Our GF grammar can parse and linearise sentences.
But we want to use our grammars in an application - not just in the terminal.
So, for our next step, we’ll make our grammars portable, so that they can be used
in other non-GF environmants
- for example, in a Rust program.
🧰 Step 07: Make the Grammars Portable
Portable Grammar Format (PGF) is to GF, what “bytecode is to Java”, and the recommended format
in which final grammar products should be distributed. The reason is that PGF grammars are
stripped from superfluous information, which results in faster processing.
⠀⠀⠀⠀⠀ On the application side, just as with Java bytecode, it should never be necessary
to edit PGF files. They are ready to be embedded in other programs.
Having written our grammar and created the AST, we now use the GF compiler
to transform it into the .pgf file format. The steps are easy:
7.1 Compile the grammars into Pizza.pgf
gf --make PizzaEng.gf PizzaFre.gf
The resulting file, Pizza.pgf, is in PGF binary code format, which means that we cannot, in contrast to the other grammars (Pizza.gf, PizzaEng.gf, and PizzaFre.gf) simply open and read it - it would just appear as gibberish.
To use the compiled Pizza.pgf file we need a runtime. GF provides runtimes
for most popular languages (Java, Python, C, C#, Haskell). These runtimes enable
developers to use the GF grammars without having to install GF on their computers.
7.2A Improving Portability with JSON: Pizza.json
The PGF file format is great, but in the web era, JSON (and XML) is surely the
most popular format for representing interchange data. So, let’s try to get our
compiled Pizza.pgf grammar converted into a Pizza.json file.
That way, it becomes portable and its content easily viewable.
# Translate Pizza.pgf into Pizza.json format
# In a folder where we have our Pizza.pgf file, run:
gf -make --output-format=json PizzaEng.gf PizzaFre.gf
Slow Compilation?: For large grammars, memory can be an issue. We can increasing the memory allocation used during compilation by using the
+RTS -K500M flag:
gf -make –output-format=json PizzaEng.gf PizzaFre.gf +RTS -K500M
7.2B An experimental script for converting PGF to JSON
I had a lot of trouble, trying to translate .pgf to .json. There seem to be
breaking changes between GF 3.11 and GF 3.12, in terms of if and how linearizations
are stored in a .pgf generated from running gf -make *.gf.
⠀⠀⠀⠀⠀After a lot of trial and error, I now have a Pyton script which appears
to be able to convert a GF 3.12 .pgf file into readable .json.
The script is called pgf2json, and I’m sharing it below:
Warning: This script has not(!) been thoroughly tested, and may generate errors.
# File: pgf2json
import pgf
import json
import sys
def pgf_to_json(pgf_file, json_file):
# Load PGF grammar
pgf_grammar = pgf.readPGF(pgf_file)
# Categories (filter to only those in the abstract grammar)
categories = ["Greeting", "Recipient"] # Explicitly list categories from Hello.gf
lincats = {}
# Use the first concrete syntax to get lincat types
first_lang = list(pgf_grammar.languages.keys())[0]
concrete = pgf_grammar.languages[first_lang]
for cat in categories:
try:
# Assume Str for known categories based on HelloEng.gf and HelloIta.gf
lincats[cat] = "Str"
except Exception as e:
lincats[cat] = f"<error: {e}>"
# Functions
functions = []
for fun in pgf_grammar.functions:
fun_name = str(fun)
functions.append(fun_name)
# Sort functions to match desired order
desired_order = ["Hello", "World", "Mum", "Friends"]
functions.sort(key=lambda x: desired_order.index(x) if x in desired_order else len(desired_order))
# Start category
startcat = str(pgf_grammar.startCat)
# Languages (concrete syntaxes)
languages = list(pgf_grammar.languages.keys())
# Linearizations per language
linearizations = {}
for lang in languages:
lin_dict = {}
concrete = pgf_grammar.languages[lang]
for fun in functions:
try:
fun_type = pgf_grammar.functionType(fun)
if fun_type.cat == "Greeting" and fun_type.hypos: # Function takes arguments (e.g., Hello : Recipient -> Greeting)
# Linearize with a dummy Recipient
expr = pgf.Expr(fun, [pgf.Expr("World", [])])
linearized = concrete.linearize(expr)
# Replace the dummy Recipient (e.g., "world") with "<Recipient>"
for recip in ["World", "Mum", "Friends"]:
recip_lin = concrete.linearize(pgf.Expr(recip, []))
linearized = linearized.replace(recip_lin, "<Recipient>")
lin_dict[fun] = linearized
else:
# Parameterless functions (e.g., World, Mum, Friends)
lin_dict[fun] = str(concrete.linearize(pgf.Expr(fun, [])))
except Exception as e:
lin_dict[fun] = f"<Linearization error: {e}>"
linearizations[lang] = lin_dict
# Build JSON data
data = {
"languages": languages,
"startcat": startcat,
"categories": categories,
"functions": functions,
"lincats": lincats,
"linearizations": linearizations
}
# Write JSON
with open(json_file, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"Successfully wrote {json_file} with {len(functions)} functions and {len(categories)} categories.")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python3 grkpgf2json.py <input.pgf> <output.json>")
sys.exit(1)
pgf_file = sys.argv[1]
json_file = sys.argv[2]
pgf_to_json(pgf_file, json_file)
run the script like so:
# In a folder containing Pizza.pgf, Pizza.json is our chosen output filename.
python3 pgf2json.py Pizza.pgf Pizza.json
🍄 Interlude: RGL
Right now, the English/French concrete syntaxes just concatenate strings, which makes parsing impossible. Instead, we should use GF Resource Grammar Library (RGL), which gives us syntax constructors for phrases, clauses, greetings, etc. That way, GF knows how to go both ways.
which gf
# Should show: /usr/local/bin/gf
# Check if RGL is installed.
ls /usr/share/gf/lib/
The RGL is typically bundled with GF or available as a separate download. If the RGL is missing, we need to install it.
# Clone the RGL repository:
#git clone https://github.com/GrammaticalFramework/gf-rgl.git ~/gf-rgl
git clone https://github.com/GrammaticalFramework/gf-rgl.git ~/tmp/gf-rgl
sudo mkdir -p /usr/local/share/gf/lib
sudo cp -r ~/tmp/gf-rgl/src/* /usr/local/share/gf/lib/
# Set the GF_LIB_PATH to include the RGL directory:
#export GF_LIB_PATH=~/gf-rgl/src
# Persist environment variable (~/.zshrc or ~/.bashrc)
# echo 'export GF_LIB_PATH=~/tmp/gf-rgl/src' >> ~/.zshrc
echo 'export GF_LIB_PATH=/usr/local/share/gf/lib' >> ~/.zshrc
source ~/.zshrc
🦀 Step 08: Use the Pizza.json Grammar in an Application
use gf_core::*;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load a PGF grammar from JSON file
let json_content = fs::read_to_string("grammars/Pizza.json")?;
let json: serde_json::Value = serde_json::from_str(&json_content)?;
let pgf: PGF = serde_json::from_value(json)?;
// Convert to runtime grammar
let grammar = GFGrammar::from_json(pgf);
// Parse an abstract syntax tree from string
let tree = grammar
.abstract_grammar
.parse_tree("Bonjour, ici la pizzeria Luigi. Que puis-je faire pour vous ?", None)
.expect("Failed to parse tree");
println!("Parsed tree: {}", tree.print());
// Linearize the tree in English
if let Some(eng_concrete) = grammar.concretes.get("PizzaEng") {
let english_output = eng_concrete.linearize(&tree);
println!("English: {}", english_output);
}
// Linearize the tree in French
if let Some(fre_concrete) = grammar.concretes.get("PizzaFre") {
let french_output = fre_concrete.linearize(&tree);
println!("French: {}", french_output);
}
Ok(())
}
🛀 End Note: What’s Next?
We’ve only scratched the surface of Grammatical Framework. The system has been
developed by sharp CS Ph.D.s and a professors for almost a decade.
⠀⠀⠀⠀⠀ One area that I’m personally looking at, is using LLMs as ‘middleware’ in a GF
gramamr dialogue system. Manually crafting grammars is quite tricky, and so LLMs
could be used to both simplify the process, but also add ‘color’ to linearizations.
🧭 Appendix: GF Commands
| short | command | action |
|---|---|---|
| i | import *.gf | compile the grammar into an internal representation |
| p | parse *.gf | turns string into an abstract syntax tree: parse “hello world” -> Hello World |
| l | linearise *.gf | turns abstract syntax trees into strings: linearize Hello World -> “hello world” |
🔥 TroubleShooting
On macOS (especially Apple Silicon), the GF source distribution includes the Python bindings, but they’re not built automatically. We’ll need to go into the right folder in the source tree, and build it ourselves.
cd /Downloads/gf-core-master/src/runtime/python
# Instsall bindings:
python3 setup.py install --user
# Verify build was successful:
python3 -c "import pgf; print(pgf.__file__)"