From claude-magik
This skill should be used when the user is writing, reviewing, or asking questions about Magik code — the programming language used in GE Smallworld GIS. Use this skill when the user mentions Magik, Smallworld, exemplars, def_slotted_exemplar, _method, _pragma, sw:rope, sw:property_list, or any Smallworld-specific constructs. Also use it when the user asks about GIS application development, Smallworld modules, or Java interop in a Magik context.
How this skill is triggered — by the user, by Claude, or both
Slash command
/claude-magik:magikThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Magik is a dynamically typed, object-oriented programming language built for Smallworld GIS (now GE Smallworld Geo Network Management). It compiles to Java bytecode and runs on the JVM. It is strictly OO and imperative — similar in spirit to Smalltalk — and is used for enterprise GIS applications in utilities and telecoms.
Magik is a dynamically typed, object-oriented programming language built for Smallworld GIS (now GE Smallworld Geo Network Management). It compiles to Java bytecode and runs on the JVM. It is strictly OO and imperative — similar in spirit to Smalltalk — and is used for enterprise GIS applications in utilities and telecoms.
# This is a single-line comment
## This is a method documentation comment (used by tooling)
a << 1.234 # "a becomes 1.234"
b +<< a # b << b + a (compound assignment)
c << "foo" + "bar" # string concatenation
Never use = for assignment. = is equality comparison only.
write("Hello, world!") # prints with newline
show(some_variable) # inspect/debug print
# Integer
x << 42
# Float
y << 3.14
# String
name << "Alice"
# Symbol (unique token, like an interned string — used heavily for identifiers)
sym << :my_symbol
escaped_sym << :|hello world|
# Boolean — NOTE: _true and _false, NOT true/false
flag << _true
flag << _false
# Null equivalent
nothing << _unset # like null/nil in other languages
# Character literal
ch << %A # the character 'A'
# Simple vector (array literal)
v << {1, 2, 3}
# Property list (ordered key-value, preserves insertion order)
pl << sw:property_list.new_with(:key1, "val1", :key2, "val2")
# Hash table (unordered)
ht << sw:hash_table.new()
# Concurrent hash map (preferred in Smallworld 5 — thread-safe and fastest)
chm << sw:concurrent_hash_map.new()
Magik uses keyword booleans — NEVER use bare true / false:
_true
_false
_maybe # tri-state Kleenean: _true, _maybe, or _false
Methods returning a Kleenean result are named with ?? suffix (e.g. inside??()). Do not use the result directly as a boolean condition — compare explicitly with _is _true / _is _false / _is _maybe.
Logical operators:
_and _or _not _xor
Local variables must be declared with _local:
_local my_count << 0
_local result << _unset
Global variables use _global (avoid in production — causes namespace pollution):
_global my_global << "some value"
Dynamic (thread-local) variables:
_dynamic !my_dynamic!
Convention: use snake_case with descriptive nouns. No camelCase. No type prefixes.
foo, its setter must be named foo<<.? and can be used directly as conditions:
_if i.odd? _then ... _endif
_true, _false, or _maybe) end in ??. Compare explicitly rather than using as a bare condition:
# inside?? returns _true if strictly inside, _false if any part outside, _maybe if on edge
_if rect.inside??(other_rect) _is _true _then ... _endif
! within their names (e.g. int!method()). This makes them easy to identify and allows special treatment by method browsers. All such methods must use classify_level=restricted.sqrt and sqrt() are entirely different methods:
# Slot-like — no brackets (behaves like reading a property):
my_obj.name
my_obj.size
# Not slot-like — with brackets (does work, has side effects, or returns multiple values):
my_obj.calculate()
my_obj.open_stream()
a_coordinate, a_rope, an_integer.new_with_corners(coord1, coord2).!: !output!, !print_length!.init() method per class. Multiple initialisers make subclassing much harder, as every subclass must find and override each one.my_object.define_shared_constant(:related_class, other_exemplar, :public)
$
_if condition
_then
# ...
_endif
_if x > 0
_then
write("positive")
_elif x = 0
_then
write("zero")
_else
write("negative")
_endif
Note: Use _is for identity (same object reference), = for value equality:
_if a _is _unset _then write("a is null") _endif
_if a = b _then write("a equals b") _endif
# While loop
_local i << 0
_while i < 10
_loop
i +<< 1
_endloop
# For-over loop (iterate a collection)
_for item _over my_collection.elements()
_loop
write(item)
_endloop
# For-over with index and value
_for k, v _over my_property_list.fast_keys_and_elements()
_loop
write(k, " => ", v)
_endloop
# General loop with _leave to break, _continue to skip
_loop
_if some_condition _then _leave _endif
_if skip_condition _then _continue _endif
_endloop
Procedures are first-class objects, assigned to variables:
my_procedure << _proc @my_procedure(a, b, c)
_return a + b + c
_endproc
x << my_procedure(1, 2, 3) # x = 6
Use >> as a short form of _return:
my_proc << _proc(x)
>> x * 2
_endproc
Magik does not have classes. It uses exemplars — a prototype-based system where new instances are clones of the exemplar.
def_slotted_exemplar(
:my_object,
{
{:slot_a, _unset},
{:slot_b, "default_value"}
},
{:parent_exemplar_a, :parent_exemplar_b} # inheritance list
)
$
Always define slot accessors using slotted_format_mixin.define_slot_access() rather than writing accessor methods by hand. Slots must only be accessed directly (.slot_name) inside init() and the generated accessor methods — all other code must go through the accessors.
my_object.define_slot_access(:slot_a, :writable, :public)
my_object.define_slot_access(:slot_b, :readable, :public)
$
_method my_object.new(val_a, val_b)
## Creates a new MY_OBJECT with VAL_A and VAL_B.
## Returns a new my_object instance.
>> _clone.init(val_a, val_b)
_endmethod
$
_private _method my_object.init(val_a, val_b)
##
.slot_a << val_a
.slot_b << val_b
>> _self
_endmethod
$
_clone creates a new instance_self refers to the current object.slot_name accesses a slot (field) via dot notation — only in init() and accessors_super calls the parent's implementationPrefer define_shared_constant over defining a constant inside a method body:
my_object.define_shared_constant(:my_const, 42, :public)
$
# Use :private for internal constants:
my_object.define_shared_constant(:int!default_size, 16, :private)
$
All method definitions must have a _pragma immediately before them with a classify_level:
_pragma(classify_level=basic, topic={my_topic})
_method my_object.do_something(param)
## Does something useful with PARAM.
## Returns the result of the operation.
_local result << .slot_a + param
>> result
_endmethod
$
Valid classify levels: basic, advanced, restricted.
basic or advancedrestricted, and the method name must start with int!_private): restricted_pragma(classify_level=restricted)
_method my_object.int!internal_helper()
## Internal helper — not part of the public API.
>> .slot_a * 2
_endmethod
$
_pragma(classify_level=restricted)
_private _method my_object.compute_value()
##
>> .slot_b.size
_endmethod
$
Use topic={code} for methods that generate bytecode or perform code generation:
_pragma(classify_level=advanced, topic={code})
_method my_object.define_dynamic_method()
##
# ...
_endmethod
$
Every _method definition must include a ## docstring immediately after the method signature (even if empty). Documentation rules:
_pragma(classify_level=basic)
_method my_object.greet(_optional name)
## Greets the user by NAME.
## If NAME is not given, defaults to "World".
## Returns _unset.
_if name _is _unset
_then
name << "World"
_endif
write("Hello, ", name)
_endmethod
$
_pragma(classify_level=restricted)
_private _method my_object.internal_helper()
## Internal helper — not part of the public API.
>> .slot_a * 2
_endmethod
$
_pragma(classify_level=basic)
_method my_object.greet(_optional name)
## Greets the user by NAME (defaults to "World").
_if name _is _unset
_then
name << "World"
_endif
write("Hello, ", name)
_endmethod
$
_pragma(classify_level=basic)
_method my_object.sum(_gather values)
## Returns the sum of all VALUES.
_local total << 0
_for v _over values.elements()
_loop
total +<< v
_endloop
>> total
_endmethod
$
Mixins add behaviour without data. They cannot be instantiated directly:
def_mixin(:my_mixin)
$
_pragma(classify_level=basic)
_method my_mixin.shared_behaviour()
## Demonstrates shared mixin behaviour.
write("I am a mixin method")
_endmethod
$
# Use in an exemplar:
sw:def_slotted_exemplar(:my_class, {}, {:my_mixin})
$
Custom iterators use _iter and _loopbody:
_pragma(classify_level=basic, topic={iter})
_iter _method my_object.even_elements()
## Yields each even element from _self.
_for a _over _self.elements()
_loop
_if a.even? _is _true
_then
_loopbody(a)
_endif
_endloop
_endmethod
$
Usage:
_for val _over my_obj.even_elements()
_loop
write(val)
_endloop
Use _protect / _protection for cleanup (like finally):
_protect
# code that may fail
risky_operation()
_protection
# always runs — cleanup here
cleanup()
_endprotect
Use _try / _when to catch conditions:
_try
risky_operation()
_when error
write("An error occurred")
_endtry
Raise a condition:
sw:condition.raise(:my_error, :string, "Something went wrong")
Always wrap production code in error handling. End users must never see raw tracebacks.
Java closeable resources (streams, writers, etc.) must be closed unconditionally. Put the close/finish call in the _protection block, not as the last step of the try body — so it runs even when an exception is raised. An empty _protection block provides no cleanup and is a bug.
_local stream << some_java_resource.open()
_protect
stream.write("data")
_protection
stream.close()
_endprotect
| Collection | Ordered? | Thread-safe? | Notes |
|---|---|---|---|
simple_vector | Yes | No | Fixed size array |
rope | Yes | No | Dynamic list |
property_list | Yes | No | Ordered key-value |
hash_table | No | No | Unordered key-value |
concurrent_hash_map | No | Yes | Preferred in SW5 |
set | No | No | Unique elements |
sorted_collection | Yes (custom) | No | Sorted by order proc |
basic_collection_mixin)These are available on all standard collection types:
col.empty? # true if no elements (does NOT clear)
col.includes?(item) # true if item is in the collection
col.includes_all?(another) # true if all elements of another are in col
col.an_element() # returns one arbitrary element, or _unset
col.elements() # iter: yields each element (safe copy)
col.fast_elements() # iter: fast, undefined if modified during loop
col.elements_satisfying(pred) # iter: yields elements where pred.invoke(e) is true
col.select(pred) # returns new collection with matching elements
col.map(fn) # returns new collection with fn.invoke(e) applied
col.reduce(fn) # folds: fn.invoke(acc, e) over all elements
col.as_simple_vector() # converts to simple_vector
col.as_sorted_collection(order) # converts to sorted_collection
Note: empty() (no ?) clears the collection. empty? (with ?) tests if it is empty.
r << sw:rope.new()
r.add_last("item1")
r.add_last("item2")
r.add_first("item0")
r.size # number of elements
r[1] # 1-origin indexed access
r.remove_first() # remove and return first element
r.remove_last() # remove and return last element
r.as_simple_vector() # convert to simple_vector
_for item _over r.elements()
_loop
write(item)
_endloop
pl << sw:property_list.new()
pl << sw:property_list.new_with(:a, 1, :b, 2)
pl[:key] << "value" # store
pl[:key] # retrieve
pl.size # count
pl.empty? # test if empty
pl.empty() # clear all entries
pl.includes?(value) # test value membership
pl.key_of(value) # find key for a value
pl.lookup_key(key) # returns item, success_flag
pl.remove_key(key) # remove entry
_for k, v _over pl.keys_and_elements()
_loop
write(k, " => ", v)
_endloop
_for k, v _over pl.fast_keys_and_elements() # fast, don't modify during loop
_loop
write(k, " => ", v)
_endloop
ht << sw:hash_table.new()
chm << sw:concurrent_hash_map.new()
chm << sw:concurrent_hash_map.new_with(:a, 1, :b, 2)
chm[:key] << "value" # store
chm[:key] # retrieve
chm.size # count
chm.empty? # test if empty
chm.empty() # clear
chm.includes?(value) # test value membership
chm.lookup_key(key) # returns item, success_flag
chm.remove_key(key) # remove entry
chm.put_if_absent(key, value) # only insert if key not already present
chm.add_all(another) # merge in all entries from another map
_for k, v _over chm.fast_keys_and_elements()
_loop
write(k, " => ", v)
_endloop
s << sw:set.new()
s.add(item) # add (no-op if already present)
s.remove(item) # returns success flag
s.includes?(item)
s.size
s.intersection(another)
s.difference(another)
s.symmetric_difference(another)
sc << sw:sorted_collection.new()
sc << sw:sorted_collection.new(_unset, _proc(a, b) >> a _cf b _endproc)
sc << sw:sorted_collection.new_from(any_collection)
sc.add(item)
sc.remove(item)
sc.includes?(item)
sc.size
sc.nth(n) # 1-origin access
sc.order_proc << new_proc # re-sort with new order
_for item _over sc.elements()
_loop
write(item)
_endloop
Prefer sw:write_string(a, b, c) over string concatenation (a + b + c) — it avoids intermediate allocations and works with any printable type:
sw:write_string("Hello, ", name, "!")
s << "hello world"
s.size # length
s.uppercase # "HELLO WORLD"
s.lowercase # "hello world"
s.titlecase # "Hello World"
s.trim_spaces # strip leading/trailing whitespace
s.split_by(%,) # split by comma character, returns rope
s.index_of_seq("world") # find substring (1-origin), _unset if not found
s.substitute_string("hello", "goodbye") # replace all occurrences
s.as_symbol() # convert to symbol
s.includes_seq?("world") # test substring presence
Use Smallworld pathname utilities for building file paths — never string concatenation:
# Build a path by descending into subdirectories:
sw:system.pathname_down(base_dir, "subdir", "file.txt")
# Build from a vector of components:
sw:system.pathname_from_components({"C:", "workspace", "myfile"})
# Appending a file extension is NOT path manipulation — sw:write_string is fine:
_local jar_name << sw:write_string(module_name, ".jar")
Magik's core libraries use British English. Always use:
initialise (not initialize)colour (not color)neighbour (not neighbor)Each top-level statement or definition in a .magik file must end with $ on its own line:
def_slotted_exemplar(:my_obj, {}, {})
$
_pragma(classify_level=basic)
_method my_obj.hello()
## Writes a greeting.
write("hi")
_endmethod
$
In the REPL prompt, $ submits the block. In files, it acts as a statement delimiter.
n << 42
n.shift(3) # n * 2^3 (bit shift)
n.factorial() # n!
n.power2() # smallest power of 2 >= n
n.as_float() # convert to float
n.as_character() # e.g. 65 -> %A
f << 3.14
f.rounded() # nearest integer (banker's rounding at .5)
f.truncated() # toward zero
f.floor() # largest integer <= f
f.ceiling() # smallest integer >= f
f.as_rational() # exact rational representation
f.is_nan?() # IEEE NaN test
f.infinite?() # +/- infinity test
f.finite?() # finite test
float.pi() # Pi constant
float.infinity() # +Infinity
# Many system objects are singletons accessed directly:
gis_program_manager # the main application manager
ds_environment # database environment
smallworld_product # product info
_if .my_slot _isnt _unset
_then
.my_slot.do_something()
_endif
result << my_obj.get_collection().elements().size
magik_rep.load_chunck(some_string.read_stream())
= for assignment — it's comparison only; use <<true/false — always _true/_falsenull/nil — always _unsetsnake_case with underscoresstrName, intCount) — use descriptive names like first_name, item_count_global casually — globals pollute the namespace; prefer locals and passed argumentshash_table in Smallworld 5 when not needed — prefer concurrent_hash_map for performance and thread safety; use property_list only when insertion order matters+ — use sw:write_string(a, b, c) insteadinit() and accessor methods — always go through define_slot_access-generated accessorsdefine_shared_constant()_protection blocks empty when a resource must be closed — an empty _protection block is a bugempty? and empty() — empty? tests emptiness; empty() clears the collectionsw:system.pathname_down() and related utilitiesinit() methods — a single initialisation entry point makes subclassing far simpler!name! naming for globals — the !name! convention is reserved for dynamic (thread-local) variablesdefine_shared_constant() to hold a reference to the related exemplarsmallworld-magik-vscode extension (Systemap)magik-tools by StevenLooman (GitHub); see also the magik-lsp plugin in this repository (plugins/magik-lsp/) for Claude Code integrationmagik_rep.load_chunck()gis_aliases file via F2-z in VS Code extensionAssignment: a << value
Compound assign: a +<< value
Equality: a = b
Identity: a _is b / a _isnt b
Not equal: a ~= b
Boolean: _true _false _maybe (Kleenean)
Null: _unset
Self: _self
Return: _return val or >> val
New instance: my_exemplar.new(...) → internally _clone.init(...)
Slot access: .slot_name (only in init/accessors)
Slot accessors: my_obj.define_slot_access(:slot, :writable, :public)
Shared constant: my_obj.define_shared_constant(:name, value, :public)
Private method: _private _method ...
Internal-public: _method my_obj.int!name() (classify_level=restricted)
Optional param: _optional param_name
Varargs: _gather param_name
Iterator method: _iter _method ... _loopbody(val) ...
Loop break: _leave
Loop continue: _continue
Pragma: _pragma(classify_level=basic)
Docstring: ## Description with PARAMS uppercase. Returns ...
Boolean method: name ending in ? (returns _true/_false, usable as condition)
Kleenean method: name ending in ?? (returns _true/_maybe/_false, compare with _is)
Slot-like method: no brackets — my_obj.name
Non-slot method: brackets — my_obj.calculate()
Setter: foo<< (paired with slot-like getter foo)
String concat: sw:write_string(a, b, c) (not a + b + c)
Empty test: col.empty? (not col.empty() which clears!)
npx claudepluginhub krn-robin/claude-magikWorked fix examples for 6 common ObjectScript LLM mistakes: Return vs Quit in loops, HTML escaping order, SQL date filters, list operations, postfix conditions. Uses Bug Pattern → Root Cause → Fix structure.
Implements Crystal macros for compile-time metaprogramming, code generation, DSLs, AST manipulation, and type-safe abstractions.
Searches Elixir packages like Ash, Phoenix, Ecto for usage rules, best practices, conventions, patterns, common mistakes, and code examples via local deps or fetch.