Guidelines for using large language models to generate, edit, and reason about Geno source code.
System Prompt Essentials
When prompting an LLM to write Geno code, include these rules at minimum:
You are writing code in Geno, a statically typed, functional-first language.
Key syntax rules:
- Use `let` for immutable bindings: `let x: Int = 5` or `let x = 5`
- Use `var` for mutable bindings: `var count: Int = 0` or `var count = 0`
- Update a `var` by assigning to it: `count = count + 1`, not `var count = count + 1`
- Function parameters and return values require type annotations
- Boolean literals are lowercase: `true`, `false`
- String concatenation uses `+` (same as arithmetic addition)
- Integer division truncates: `7 / 2 = 3`
- Functions end with `end func`, loops with `end for` / `end while`
- Every function needs at least one `example` clause
- Functions with 3+ parameters require named arguments at call sites
- Use `Option[T]` instead of null, `Result[T, E]` for errors
- Comments: `// line` or `/* block */`
- Doc comments: `/// text`
- Do not name anything after a Python builtin (`len`, `id`, `map`, `abs`,
`format`, `type`, `list`, `str`, ...) — these are reserved runtime names and
are rejected when the program is compiled
Common LLM Mistakes
1. Using let mut instead of var
Wrong:
let mut count: Int = 0
Correct:
var count: Int = 0
2. Using True/False (Python-style booleans)
Wrong:
return True
Correct:
return true
3. Using ++ for string concatenation
Wrong:
let greeting: String = "Hello, " ++ name
Correct:
let greeting: String = "Hello, " + name
4. Using end as a parameter name
end is a keyword. Use stop instead:
Wrong:
func substring(text: String, start: Int, end: Int) -> String
Correct:
func substring(text: String, start: Int, stop: Int) -> String
5. Forgetting named arguments for 3+ parameters
Wrong:
substring("hello world", 0, 5)
Correct:
substring(text: "hello world", start: 0, stop: 5)
6. Forgetting example clauses
Wrong:
func double(n: Int) -> Int
return n * 2
end func
Correct:
func double(n: Int) -> Int
example (3) -> 6
example (0) -> 0
return n * 2
end func
7. Zero-arg example syntax
Both forms are accepted for a function that takes no parameters:
example () -> 42
example -> 42
Do not write the input as a value the function never takes — the example input must match the parameter list.
8. Using get(list, index) instead of bracket indexing
Wrong:
let first: Int = get(xs, 0)
Correct:
let first: Int = xs[0]
9. Missing then after if
Wrong:
if x > 0
return x
end if
Correct:
if x > 0 then
return x
end if
10. Missing do after for/while
Wrong:
for i: Int in range(0, 10)
println(i)
end for
Correct:
for i: Int in range(0, 10) do
println(i)
end for
11. Reserved runtime names
geno check, geno test, editor diagnostics, and the default geno run reject
bindings that would shadow the compiled runtime in programs whose entrypoint
defines main. Targetless library checks remain permissive. The names come from the backend's host builtins, so
len, id, map, abs, format, type, list, print, and str are not
usable as function, parameter, or local names.
Wrong:
func trim_to(s: String, len: Int) -> String
Correct:
func trim_to(s: String, count: Int) -> String
12. Destructuring tuples in match
Tuple patterns are supported in match arms, so a pair does not need to be let-destructured first:
match split_once(line, ":") with
| Some((key, value)) -> return key
| None -> return ""
end match
13. Multiline sum types
Variant lists may span lines, and a leading | is allowed:
type Tx =
| Deposit(amount: Int)
| Withdraw(amount: Int)
14. Matching parse_int / parse_float as Result
parse_int returns Option[Int] and parse_float returns Option[Float].
They never produce Ok/Err, so a Result-style match fails to typecheck.
Wrong:
match parse_int(arg) with
| Ok(n) -> return n
| Err(e) -> return 0
end match
Correct:
match parse_int(arg) with
| Some(n) -> return n
| None -> return 0
end match
To surface a parse failure as a Result, wrap the branches yourself:
match parse_int(arg) with
| Some(n) -> return Ok(n)
| None -> return Err("not a number: " + arg)
end match
15. Naming a local result in a function with ensures
In a function with an ensures clause, result names the return value. A local
binding of the same name is rejected, because ensures result would be
ambiguous. Accumulators are the usual place this bites; name them acc or out.
Wrong:
func total(n: Int) -> Int
ensures result >= 0
example (3) -> 3
var result: Int = 0
var i: Int = 0
while i < n do
result = result + i
i = i + 1
end while
return result
end func
Type Error: `result` is reserved in functions with ensures clauses; rename this
binding so `ensures result` unambiguously refers to the return value
Correct:
func total(n: Int) -> Int
ensures result >= 0
example (3) -> 3
var acc: Int = 0
var i: Int = 0
while i < n do
acc = acc + i
i = i + 1
end while
return acc
end func
A function without ensures may use result as an ordinary name.
16. Updating one element of a list
Lists are immutable, but replacing an element does not mean rebuilding the list
by hand. set_at is a prelude builtin -- no import -- and returns a new list:
set_at(list: List[T], index: Int, value: T) -> List[T]
Wrong -- rebuilding around the index:
func swap(xs: List[Int], i: Int, j: Int) -> List[Int]
example ([1, 2, 3], 0, 2) -> [3, 2, 1]
var out: List[Int] = []
var k: Int = 0
while k < length(xs) do
if k == i then
out = append(out, xs[j])
else
if k == j then
out = append(out, xs[i])
else
out = append(out, xs[k])
end if
end if
k = k + 1
end while
return out
end func
Correct:
func swap(xs: List[Int], i: Int, j: Int) -> List[Int]
example ([1, 2, 3], 0, 2) -> [3, 2, 1]
let a: Int = xs[i]
let b: Int = xs[j]
return set_at(list: set_at(list: xs, index: i, value: b), index: j, value: a)
end func
set_at has three parameters, so its call sites need named arguments. It raises
if the index is out of range. For code that mutates in a loop rather than
threading a new list through each step, Vec[T] and vec_set are the better
fit.
17. Float example values and how close is close enough
Float examples allow small rounding differences, but a convenience rounding
can still fail. Keep the full value printed by a real run.
Wrong -- right to five digits, which is nowhere near close enough:
func ratio(a: Float, b: Float) -> Float
example (160.0, 7.0) -> 22.857
return a / b
end func
Correct:
func ratio(a: Float, b: Float) -> Float
example (160.0, 7.0) -> 22.857142857142858
return a / b
end func
geno test and the run_harness_from_source and run_harness_from_compiled
helpers compare the same way, including inside lists, tuples, maps and
constructors. Two Int values must be equal. When either side is a Float, the two must agree to a relative tolerance
of 1e-9, with an absolute floor of 1e-12 near zero. An expectation rounded to
ten or more significant digits therefore passes at any magnitude: rounding
16000000.0 / 7.0 to 2285714.28571 passes, while 22.857 above is off by about
six parts in a million and fails.
The reliable habit is not to compute the expected value mentally at all. Run the
function, take the value it prints, and keep it. Where the domain allows it,
prefer Int arithmetic instead and sidestep the question -- money is the common
case, since working in cents makes every expected value exact.
18. requires and Err examples on the same function
A requires clause is checked before the body runs, so a function cannot both
contract its happy path and example its rejection path. An example whose
expected value is Err(...) for an input the precondition excludes fails: the
precondition rejects the input before the body can return the Err.
Wrong -- requires and the Err example contradict each other:
func to_roman(n: Int) -> Result[String, String]
requires n >= 1
example (1) -> Ok("I")
example (0) -> Err("out of range")
if n < 1 then
return Err("out of range")
end if
return Ok("I")
end func
Correct -- keep the public Result API free of preconditions and validate in
the body, and put requires on the in-range helper it calls:
func roman_digit(n: Int) -> String
requires n >= 1
example (1) -> "I"
return "I"
end func
func to_roman(n: Int) -> Result[String, String]
example (1) -> Ok("I")
example (0) -> Err("out of range")
if n < 1 then
return Err("out of range")
end if
return Ok(roman_digit(n))
end func
The rule of thumb: a function that returns Result validates its own input, so
it takes no requires. A helper that assumes valid input takes the requires
and is never called with anything else.
19. Multi-file projects still need import
Listing a module in geno.toml makes it part of the project. It does not bring
its declarations into scope. Every module that uses another module's functions
imports it, and a missing import fails at the call site -- where the name is
used, not where the module is declared.
Given files = ["Lib", "Main"] in geno.toml and a double defined in
Lib.geno, this fails:
func main() -> Int
return double(4)
end func
Correct:
import Lib
func main() -> Int
return double(4)
end func
A plain import Lib puts the module's functions in scope unqualified, so
double(4) works. The qualified form Lib.double(4) is also valid and is worth
preferring when two modules define the same name.
Prompting Patterns
Generate a function
Write a Geno function that [description].
Requirements:
- Include example clauses demonstrating edge cases
- Use named arguments for calls with 3+ parameters
- Prefer immutable `let` bindings over mutable `var`
- Use pattern matching (`match ... with`) over nested if/else chains
Translate from Python
Translate this Python function to Geno:
[Python code]
Remember:
- `True`/`False` → `true`/`false`
- `def` → `func ... end func`
- `elif` → `else if ... then`
- `for x in range(n)` → `for x: Int in range(0, n) do ... end for`
- `None` → use `Option[T]` with `Some(value)` / `None`
- Add type annotations to all parameters and return types
- Add example clauses
Fix Geno code
Fix this Geno code. Common issues to check:
- `var` not `let mut` for mutable bindings
- `true`/`false` not `True`/`False`
- `+` not `++` for string concatenation
- Named arguments for 3+ parameter calls
- `then` after `if`, `do` after `for`/`while`
- `end func`/`end if`/`end for`/`end while` block terminators
- `stop` not `end` as parameter name (keyword conflict)
Machine-Readable Specification
A complete machine-readable language specification is available at
spec.json in the repository root. It includes all keywords, types,
operators (with precedence), syntax templates, and built-in function
signatures. Feed this to LLMs as structured context for accurate code
generation.
Providing Context for Code Generation
For best results, include in the LLM context:
spec.json— full language spec (keywords, types, builtins)- Example code — working Geno programs from
examples/ - Standard library — files in
geno/std/for available functions - This guide — common pitfalls to avoid
Compilation Targets
Geno compiles to Python and JavaScript. When asking an LLM to reason about compiled output:
- Python target: uses type-directed optimization with safety wrappers where needed, resulting in ~1.1x median overhead vs hand-written Python on the 30 measured benchmark problems
- JavaScript target: used for browser apps with the init/update/render lifecycle
- Hosted runtime: executes Geno source via HTTP POST to
/run
Benchmark Reference
Compiled Geno Python runs at a median 1.09x overhead compared to
equivalent hand-written Python (mean 1.23x; 30 of 77 problems measured,
47 skipped as too noisy for stable timing ratios).
The compiler uses type-directed optimization to emit raw arithmetic for
typed operands and only inserts safety wrappers (_safe_add,
_safe_index) where types cannot be statically proven safe.
See benchmarks/RESULTS.md for detailed per-problem data.
Rendered from docs/llm-prompting.md in the Geno repository.