AOTLogoSharp 3.0.0
dotnet add package AOTLogoSharp --version 3.0.0
NuGet\Install-Package AOTLogoSharp -Version 3.0.0
<PackageReference Include="AOTLogoSharp" Version="3.0.0" />
<PackageVersion Include="AOTLogoSharp" Version="3.0.0" />
<PackageReference Include="AOTLogoSharp" />
paket add AOTLogoSharp --version 3.0.0
#r "nuget: AOTLogoSharp, 3.0.0"
#:package AOTLogoSharp@3.0.0
#addin nuget:?package=AOTLogoSharp&version=3.0.0
#tool nuget:?package=AOTLogoSharp&version=3.0.0
AOTLogoSharp
Logo programming language for managed world.
What's AOTLogoSharp?
AOTLogoSharp is a .NET implementation of the Logo programming language which is based on a hand-written recursive descent parser.
Why AOTLogoSharp?
AOTLogoSharp is fully compatible with Native AOT and trimming. You can publish your application as a single native executable with minimal size and fast startup, without any JIT overhead. This makes it ideal for scenarios where deployment size and startup speed matter, such as command-line tools, educational apps, or embedded drawing pipelines.
The assembly names remain LogoSharp.dll and LogoSharp.Drawing.dll, and the namespace stays as LogoSharp / LogoSharp.Drawing, so existing code that references the original LogoSharp can migrate by simply swapping the NuGet package.
How to use AOTLogoSharp?
It is very simple and straightforward to use AOTLogoSharp in your .NET projects that is either based on .NET Framework 4.8, .NET Standard 2.0, .NET Standard 2.1, or .NET 10. This means that you can make your AOTLogoSharp app working with .NET Core and thereby provides the cross-platform capability.
- Install AOTLogoSharp NuGet Package, for example:
Install-Package AOTLogoSharp -Version 3.0.0
- Write your first app:
using LogoSharp;
static void Main(string[] args)
{
var logo = new Logo();
logo.Forward += (s, e)
=> Console.WriteLine($"Forwarded {e.Steps} steps.");
logo.Execute("FD 102");
}
- Run your app, you should get the message
Forwarded 102 steps.on your console
Basically, a Logo program code is provided to the Execute method of the logo instance, AOTLogoSharp will execute the code and emit the events. Therefore, event handlers that subscribe to a particular event will be hit once the execution of the code emits the subscribed event.
The Logo class exposes the HeadingMode property to control the semantics of SETH / SETHEADING:
"logo"(default): 0° = north, clockwise. This matches the standard Logo language specification."standard": 0° = east, counter-clockwise. This matches the mathematical polar coordinate convention.
var logo = new Logo();
logo.HeadingMode = "standard"; // switch to mathematical heading semantics
logo.Execute("SETH 0"); // now points east instead of north
The Logo class also exposes the WaitMode property to control the unit of the WAIT command:
"logo"(default): 1 tick = 1/60 second (~16.67 ms), following traditional Logo specifications."ms"/"standard": 1 tick = 1 millisecond.
var logo = new Logo();
logo.WaitMode = "logo"; // WAIT 1 ≈ 16.67 ms
logo.Execute("WAIT 60"); // waits ~1 second
Coordinate System
AOTLogoSharp uses a Cartesian coordinate system: the origin is at the center of the canvas, the x-axis points to the right, and the y-axis points upward. The turtle starts at the origin and faces north (the positive y direction). This differs from the common computer-graphics convention where the origin is at the top-left and the y-axis points downward, so please interpret coordinates as in a mathematical Cartesian plane.
PRINT / TYPE / SHOW Commands
AOTLogoSharp supports three text output commands: PRINT, TYPE, and SHOW. They share the same argument syntax but differ in formatting:
- PRINT arg — output with a trailing newline. String literals have their leading
"stripped. Lists are space-joined without brackets. Numbers useScalar.Format()(e.g.3+4i,3.5,5). - TYPE arg — identical to
PRINTbut no trailing newline. Multiple arguments (via(WORD ...)or mixed concat) are concatenated without separators. - SHOW arg — identical to
PRINTbut lists are displayed with brackets:[1 2 3].
All three support the same expression forms:
AOTLogoSharp supports the PRINT command with multiple forms:
PRINT "text— output a string literal (everything after"up to the next whitespace).PRINT 42— output a numeric value.PRINT [word1 word2 ...]— output a list literal as a space-separated sentence.PRINT (expr)— output an expression result (when no[...]appears inside(...)).PRINT ([text] :var)— mixed concat: list + variable, joined with spaces.PRINT ([text] + :var)— mixed concat: list + variable, joined without spaces (+joins).PRINT (:var [text] :var)— mixed concat: variable + list + variable, joined with spaces.PRINT (:var + [text] + :var)— mixed concat: variable + list + variable, joined without spaces.PRINT ({expr} + [text] + :var)— mixed concat: computed expression + list + variable.PRINT (SE 1 2 3)— enhanced: multi-arg SE/WORD/LIST function call.
Mixed concatenation mode activates automatically whenever any [...] list literal appears inside (...). In this mode:
+acts as a no-space joiner (not arithmetic addition).- Adjacent elements without
+are joined with a single space. [...]contents are treated as literal text. Adjacent characters inside[...]that have no whitespace between them in the source are kept together (e.g.[π/]→π/, notπ /).{expr}can be used for computed values (e.g.{:x + 2}evaluates the addition first).- When no
[...]appears,(...)behaves as a normal arithmetic expression:PRINT (1 + 2)outputs3.
Example:
MAKE "x 3
MAKE "y 2
PRINT ([The answer is] :x) ; The answer is 3
PRINT ([Answer:] + :x) ; Answer:3
PRINT (:x [π /] :y) ; 3 π / 2
PRINT (:x + [π/] + :y) ; 3π/2
PRINT ({:x + 2} + [π/] + :y) ; 5π/2
PRINT (:x + [π/] + {:y + 5}) ; 3π/7
PRINT (:x + [π/] + {:y + 5} + [≈] + {FORM :x * PI / (:y + 5) 6 4})
; 3π/7≈1.3464
PRINT ([COS 45=] + {COS 45}) ; COS 45=0.707106781186548
PRINT (SE 1 2 3) ; 1 2 3
PRINT (WORD "A "B "C) ; ABC
PRINT (LIST "A "B "C) ; A B C
Each PRINT invocation fires the Print event, which is exposed as EventHandler<PrintEventArgs>. The Text property carries the rendered text. String literal contents preserve the user's original casing, so PRINT "Hello outputs Hello exactly as written, even though keywords such as PRINT are matched case-insensitively.
using LogoSharp;
var logo = new Logo();
logo.Print += (s, e) => Console.WriteLine($"[print] {e.Text}");
logo.Execute(@"
PRINT [Test OK]
PRINT ""Hello World
PRINT 3.14
");
TYPE fires the Type event (same PrintEventArgs); SHOW fires the Show event. In headless mode, PRINT/SHOW write a line to stdout, while TYPE writes without a trailing newline.
Output format comparison:
| Input | PRINT |
TYPE |
SHOW |
|---|---|---|---|
42 |
42 |
42 |
42 |
3+4i |
3+4i |
3+4i |
3+4i |
"Hello |
Hello |
Hello |
Hello |
[1 2 3] |
1 2 3 |
1 2 3 |
[1 2 3] |
[1 "two [3 4]] |
1 two [3 4] |
1 two [3 4] |
[1 two [3 4]] |
String concatenation with + in parentheses: When a (...) expression contains both string literals ("X) and +, the + acts as a no-space string joiner instead of arithmetic addition. Pure numeric (1 + 2 + 3) still evaluates to 6.
TYPE ("A + "B + "C) ; outputs: ABC (string concat, no separator, no newline)
TYPE ("1 + "2 + "3) ; outputs: 123
PRINT (1 + 2 + 3) ; outputs: 6 (numeric addition)
MAKE Command and Types
MAKE assigns a value to a variable. The first argument is always the variable name (a quoted identifier "name); the second argument determines the value's type. No implicit type conversion occurs between numbers, strings, and lists — each type stays exactly as assigned.
MAKE "N 42 ; number (integer)
MAKE "PI 3.14159 ; number (real)
MAKE "Z 3+4i ; number (complex)
MAKE "S "Hello ; string
MAKE "L [1 2 3] ; list
MAKE "MIX [1 "two [3 4]] ; heterogeneous list (number + string + sublist)
Key rules:
- The first argument is always a variable name (
"name), never an expression. MAKEsets one variable at a time — you cannot assign multiple variables in one call.- Variable-to-variable assignment (
MAKE "B :A) performs a deep copy: later changes toAdo not affectB. - Function return values can be assigned:
MAKE "FN {GETNUM}orMAKE "FL {GETLIST}. - Numbers, strings, and lists are not implicitly convertible. Use the Type Conversion Functions below.
MAKE "A 100
MAKE "B :A ; B = 100 (deep copy)
MAKE "A 200 ; A = 200, B still 100
PRINT :B ; 100
TO GETLIST
OUTPUT [x y z]
END
MAKE "FL {GETLIST}
SHOW :FL ; [x y z]
List Indexing, Slicing & Reverse (C#-style)
AOTLogoSharp supports C#-style 0-based indexing, range slicing, and ^ from-end indexing on lists. All syntax mirrors the C# Index and Range types. Slicing always returns a new list (deep copy); the original is never mutated.
Basic Indexing
:var[n] — 0-based index. :var[0] is the first element, :var[len-1] is the last.
MAKE "x [10 20 30 40 50]
PRINT :x[0] ; 10
PRINT :x[4] ; 50
From-End Indexing (^)
:var[^n] — count from the end. ^1 is the last element, ^2 is second-to-last, etc.
PRINT :x[^1] ; 50 (last)
PRINT :x[^2] ; 40 (second-to-last)
Range Slicing (..)
:var[start..end] — extract a sub-list from start inclusive to end exclusive (C# Range semantics).
SHOW :x[1..3] ; [20 30] (index 1, 2)
SHOW :x[0..2] ; [10 20]
SHOW :x[2..] ; [30 40 50] (from index 2 to end)
SHOW :x[..2] ; [10 20] (from start to index 2, exclusive)
SHOW :x[..] ; [10 20 30 40 50] (full copy)
Combined ^ + .. Slicing
^ and .. can be freely combined:
SHOW :x[^2..] ; [40 50] (last 2 elements)
SHOW :x[..^1] ; [10 20 30 40] (all except last)
SHOW :x[^3..^1] ; [30 40] (from 3rd-to-last to 2nd-to-last)
Reverse Slice (::-1)
:var[::-1] — reverse the list (deep copy, original unchanged). Can be combined with .. and ^:
SHOW :x[::-1] ; [50 40 30 20 10]
Multi-Dimensional Indexing
Comma-separated indices for nested (2D, 3D, …) lists. Each dimension independently supports all indexing syntax:
MAKE "m [[1 2 3] [4 5 6] [7 8 9]]
PRINT :m[0,0] ; 1
PRINT :m[1,2] ; 6
PRINT :m[2,^1] ; 9 (last element of last row)
PRINT :m[^1,0] ; 7 (first element of last row)
Multi-dimensional reverse:
MAKE "m2 [[1 2] [3 4]]
SHOW :m2[::-1] ; [[3 4] [1 2]] (reverse rows)
SHOW :m2[::-1,::-1] ; [[4 3] [2 1]] (reverse both dimensions)
Chained Indexing
Index results can be further indexed: :var[i][j] is equivalent to :var[i,j].
MAKE "n [[1 2] [3 4] [5 6]]
PRINT :n[1][0] ; 3
PRINT :n[2][1] ; 6
Index in Expressions
Indexed values participate in all expressions:
PRINT :x[0] + :x[1] ; 30
PRINT :x[^1] - :x[0] ; 40
PRINT :m[0,0] + :m[1,1] + :m[2,2] ; 15
Type Conversion Functions
Because there is no implicit conversion between numbers, strings, and lists, AOTLogoSharp provides four explicit conversion functions. All are called with braced expression syntax {FUNC arg}.
| Function | Input | Output | Notes |
|---|---|---|---|
{STRING expr} |
number / string / list | string | Number → Scalar.Format(); string → as-is; list → space-joined (no brackets) |
{INT expr} |
string / number | number (long) | String parsed as double then truncated toward zero; number truncated; not a TRUNC alias; does not support complex or list |
{DOUBLE expr} |
string / number | number (real) | String parsed as double; complex → real part (imaginary discarded); does not support list |
{COMPLEX expr} |
string / number | number (complex) | String parsed via ParseComplexString; number → as-is |
INT truncates toward zero (like Math.Truncate), not toward negative infinity. {INT -7.8} returns -7, {INT 45.9} returns 45.
COMPLEX string formats accepted by ParseComplexString: 3, 3.5, 4i, -2i, 3+4i, 3-4i, 1+i, 1-i, i, -i, 1e2+3i. "3+4i is now parsed correctly — the + inside quoted identifiers is no longer split into separate tokens.
MAKE "N 42
PRINT {STRING :N} ; 42
PRINT {INT "123} ; 123
PRINT {INT "45.9} ; 45 (truncated toward zero)
PRINT {INT "-45.9} ; -45
PRINT {DOUBLE "3.14} ; 3.14
MAKE "C 5+12i
PRINT {DOUBLE :C} ; 5 (imaginary discarded)
SHOW {COMPLEX "5} ; 5
SHOW {COMPLEX "-2i} ; -2i
SHOW {COMPLEX "i} ; i
SHOW {COMPLEX "1-i} ; 1-i
; Conversion chain: string → DOUBLE → STRING
MAKE "TXT "3.14159
MAKE "VAL {DOUBLE :TXT}
PRINT {STRING :VAL} ; 3.14159
FILL and SETFLOODCOLOR
SETFLOODCOLOR sets the flood-fill color used by the FILL command. FILL fills the closed region surrounding the turtle's current position with the current flood color.
SETFLOODCOLOR 4 ; integer 0–15 (standard Logo color index)
SETFLOODCOLOR [255 0 0] ; list (three components, 0–255)
SETFLOODCOLOR [255 0 0 128] ; list (four components with alpha)
FILL ; 填充海龟所在闭合区域
SETFLOODCOLOR fires the SetFloodColor event (EventHandler<FloodColorEventArgs>); FILL fires the Fill event (EventHandler<FillEventArgs>). In headless mode these events drive the Skia canvas flood-fill implementation.
LOCAL Command
LOCAL declares one or more variables as local to the current procedure scope. Local variables are released automatically when the procedure returns.
TO DEMO
LOCAL "X ; declare X as local
MAKE "X 10
PRINT :X ; 10
END
TO DEMO2
LOCAL [P Q] ; declare P and Q as local in one call
MAKE "P 1
MAKE "Q 2
PRINT (:P + :Q) ; 3
END
Notes:
LOCAL "Xdeclares a single local variable;LOCAL [P Q]declares multiple.- Local variables shadow same-named global variables within the procedure.
- Declared locals are initialized to
undefineduntilMAKEassigns a value.
SE / WORD / LIST
AOTLogoSharp supports three string concatenation functions accessible both as braced expressions and via (SE ...) / (WORD ...) / (LIST ...) call syntax:
- SE arg1 arg2 ... — join arguments with spaces.
(SE 1 2 3)→1 2 3,(SE "A "B "C)→A B C. Alias: SENTENCE. - WORD arg1 arg2 ... — join arguments without spaces.
(WORD "A "B "C)→ABC,(WORD 1 2 3)→123. - LIST arg1 arg2 ... — equivalent to SE (space-joined) for FMSLogo compatibility.
These can be used inside PRINT / LABEL parentheses: PRINT (SE 1 2 3) outputs 1 2 3. They also work as standalone braced functions: MAKE "s {WORD "A "B "C}. String arguments require a leading " (e.g. "A); numeric arguments do not (e.g. 1).
LABEL Command
LABEL draws text directly on the canvas at the turtle's current position. Aliases: TURTLETEXT, TT. It supports the same expression syntax as PRINT:
LABEL "text— single word string literal.LABEL :var— variable value.LABEL [word1 word2 ...]— list literal, space-separated.LABEL (expr)— expression result (when no[...]appears inside(...)).LABEL ([text] + :var)— mixed concatenation (same rules as PRINT).LABEL (:var [text] :var)— variable + list + variable, joined with spaces.LABEL (:var + [text] + :var)— variable + list + variable, joined without spaces.LABEL ({expr} + [text] + :var)— computed expression + list + variable.
The same mixed concatenation mode rules apply: when any [...] appears inside (...), + becomes a no-space joiner, {expr} evaluates before concatenation, and elements without + are space-separated.
LABEL text is always rendered left-to-right on the canvas, regardless of the turtle heading. This differs from MSWLogo where label text rotates with the turtle heading.
LABEL color follows SETPENCOLOR: the DrawLabel event snapshots the current SETPENCOLOR / SETPC R/G/B at the moment LABEL fires and ships them to the host. The host persists the color in a per-label LabelRecord, and both the headless Save() PNG exporter and the Avalonia GUI (TurtleCanvas LabelsDrawOperation) draw the text using that recorded color — there is no hard-coded black. So SETPENCOLOR [255 0 0] followed by LABEL [red] produces red text, and a subsequent SETPENCOLOR [0 255 0] followed by LABEL [green] still produces red text for the first label and green for the second. Each LABEL binds to the pen color at the instant it executes, and the colors are independent.
Unicode font fallback: LABEL (and the headless Save() PNG export) uses per-character font fallback. The default primary typeface covers CJK characters (Microsoft YaHei on Windows, PingFang SC on macOS/iOS, Noto Sans CJK SC on Android/Linux). When a custom font is set via SETFONT, that font becomes the primary typeface. For characters not present in the primary font (e.g. ≈, ∞, ∑, emoji), SKFontManager.MatchCharacter automatically finds a system font that contains the glyph. This ensures mixed-script text like 3π/7≈1.3464, Chinese text like 中文测试, emoji like 😊, and mixed SETFONT + emoji text all render correctly on the canvas.
Example:
MAKE "x 3
MAKE "y 2
PU
LABEL ([The answer is] :x) ; The answer is 3
BK 15
LABEL ([Answer:] + :x) ; Answer:3
BK 15
LABEL (:x [π /] :y) ; 3 π / 2
BK 15
LABEL (:x + [π/] + :y) ; 3π/2
BK 15
LABEL ({:x + 2} + [π/] + :y) ; 5π/2
BK 15
LABEL (:x + [π/] + {:y + 5}) ; 3π/7
BK 15
LABEL (:x + [π/] + {:y + 5} + [≈] + {FORM :x * PI / (:y + 5) 6 4}) ; 3π/7≈1.3464
BK 15
LABEL ([COS 45=] + {COS 45}) ; COS 45=0.707106781186548
BK 15
LABEL (SE 1 2 3) ; 1 2 3
BK 15
LABEL (WORD "A "B "C) ; ABC
BK 15
LABEL (LIST "A "B "C) ; A B C
HOME
PD
SETFONT Command
SETFONT sets the font used by subsequent LABEL commands. Syntax:
SETFONT "fontname size attributes
SETFONT [fontname] size attributes
- fontname: A quoted literal (either double-quote
"or brackets[]), e.g."宋体,"Arial,[Times New Roman]- Multi-word font names (containing spaces) MUST use brackets:
"Microsoft YaHeionly capturesMicrosoft; you must write[Microsoft YaHei] - Empty brackets
[]reset to the default CJK font:SETFONT [] 0 0restores the default 14px CJK font
- Multi-word font names (containing spaces) MUST use brackets:
- size: Positive real number specifying the font size in pixels (0 means use default 14px)
- attributes: Bitmask integer (PCLogo-compatible), can be combined by addition: | Value | Attribute | |---|---| | 1 | Bold | | 2 | Italic | | 4 | Underline | | 8 | Strikeout |
Example:
SETFONT "宋体 24 0 ; Regular Song (serif), 24px
LABEL [中文标题]
SETFONT "Arial 18 1 ; Bold Arial, 18px
LABEL [Bold Title]
SETFONT [Courier New] 16 2 ; Italic Courier New, 16px
LABEL [Italic Monospace]
SETFONT [Times New Roman] 20 3 ; Bold Italic Times New Roman, 20px
LABEL [Bold Italic Serif]
SETFONT "Arial 22 4 ; Underlined Arial, 22px
LABEL [Underlined Text]
SETFONT "Arial 20 12 ; Underline + Strikeout (4+8), 20px
LABEL [Underline+Strike]
SETFONT [] 0 0 ; Reset to default CJK font
LABEL [Default restored]
Font fallback: If the specified font is unavailable on the current system, it automatically falls back to built-in CJK fonts (Windows: Microsoft YaHei, macOS: PingFang SC, Linux: Noto Sans CJK SC). Serif-style fonts fall back to serif, sans-serif-style fonts fall back to sans-serif.
SETFONT fires the SetFont event (EventHandler<SetFontEventArgs>) with FontName, FontSize, and Style properties. The host calls turtle.SetFont(fontName, fontSize, style) to apply it.
LABEL default font size: When SETFONT is not called, LABEL uses a default font size of 14px (scaled with the canvas scale factor).
Clear-screen resets font: After CLEARSCREEN / CS / CLS / DRAW / CLEARSCR, the SETFONT state is reset to defaults (built-in CJK font, 14px, regular style), together with the pen defaults. The HOME command does not change the font setting.
FORM Command
FORM number width decimal-places is a three-argument formatting function that produces a right-aligned fixed-width string (FMSLogo-compatible). If the formatted number is shorter than the requested width, leading spaces are added.
PRINT FORM 3.14159265 6 2→3.14LABEL FORM :x 4 1→ draws e.g.1.2or-0.6on the canvas.{FORM 1.5 4 2}→1.50(standalone braced expression).- Complex:
PRINT FORM 3 + 4i 16 2→3.00+4.00i(decimal precision applies to both real and imaginary parts; total width is the complex string length + padding spaces).
FORM is not a variadic function — it always requires exactly three arguments. The first argument must be a numeric value (literal, variable, or expression); passing a string (e.g. FORM "text 6 2 or FORM text 6 2) is not valid. It works both as PRINT FORM ... / LABEL FORM ... (parser-level shorthand) and as {FORM ...} (general braced expression).
Error Reporting
Runtime errors (for example, calling an undefined procedure) are reported with a precise [line:column] location in the source program. When multiple errors occur in a single Execute call, the engine aggregates them into a single RuntimeException whose Message contains every error on its own line, so a host UI can display each error as a separate list item.
STOP Command and Recursion/Loop Limits
STOP is a built-in keyword that immediately exits the innermost running procedure. It does not fire any event — instead it throws an internal StopException that the procedure dispatcher swallows, so a host UI does not see a crash. STOP outside any procedure has no effect (the exception escapes without anything to catch it and is rethrown as a normal RuntimeException).
The engine guards against runaway Logo programs with two public properties on Logo:
MaxRepeatIterations(default10000) — caps the count of a singleREPEATand the per-step iterations of aWHILEloop. Also applies toFOR: the expected iteration count is pre-computed from start/limit/step and rejected if it exceeds this limit. Each nestedFORis independently capped, so the actual total work of nested loops is bounded byMaxRepeatIterations^depth. For high-count non-nested loops, preferREPEAToverFORsinceREPEATis simpler and has no control variable overhead. Exceeding the limit throwsRuntimeExceptionwith aStackOverflowExceptionprefix.MaxCallStackDepth(default500) — caps the recursion depth of user-defined procedure calls.
var logo = new Logo
{
MaxRepeatIterations = 1000000, // allow long-running drawings
MaxCallStackDepth = 2000 // allow deeper recursion
};
logo.Execute("REPEAT 1000000 [FD 1]");
For an additional process-level safety net, the static LogoSharp.MemoryGuard helper watches the process's private memory and exits the process as soon as it crosses a threshold. This catches scenarios that pure language-level limits cannot (e.g. runaway AST growth from pathological input):
using LogoSharp;
// Start a 1 GiB watchdog (defaults: 1024 MB threshold, 1000 ms check interval).
MemoryGuard.Start(thresholdMB: 1024, checkIntervalMs: 1000);
// ... run Logo code ...
MemoryGuard.Stop();
For more information about how to use AOTLogoSharp, please refer to the LogoSharp.Drawing project.
Using AOTLogoSharp.Drawing
AOTLogoSharp.Drawing provides a Turtle class that renders Logo programs to images. It is also available as the AOTLogoSharp.Drawing NuGet package.
- Install AOTLogoSharp.Drawing NuGet Package, for example:
Install-Package AOTLogoSharp.Drawing -Version 3.0.0
- Write your first app:
using LogoSharp;
using LogoSharp.Drawing;
using SkiaSharp;
static void Main(string[] args)
{
var turtle = new Turtle();
var logo = new Logo();
// Wire Logo events to the Turtle
logo.TurnLeft += (s, e) => turtle.Left(e.Angle);
logo.TurnRight += (s, e) => turtle.Right(e.Angle);
logo.SetHeading += (s, e) => turtle.Angle = e.Angle;
logo.Forward += (s, e) => turtle.MoveForward(e.Steps);
logo.Backward += (s, e) => turtle.MoveBackward(e.Steps);
logo.PenUp += (s, e) => turtle.PenStatus = PenStatus.Up;
logo.PenDown += (s, e) => turtle.PenStatus = PenStatus.Down;
logo.SetPenColor += (s, e) =>
{
turtle.SetPenColor(e.R, e.G, e.B);
var color = Color.FromRgb(
(byte)Math.Clamp(e.R, 0, 255),
(byte)Math.Clamp(e.G, 0, 255),
(byte)Math.Clamp(e.B, 0, 255));
// Key: UI actions must in UI thread
Dispatcher.UIThread.Invoke(() =>
{
PenColor = new SolidColorBrush(color);
StrokeColor = color;
});
};
logo.SetPenSize += (s, e) => turtle.SetPenWidth(e.Width);
logo.Delay += (s, e) => turtle.DelayMilliseconds = e.Milliseconds;
logo.Wait += (s, e) =>
{
turtle.WaitMode = logo.WaitMode;
turtle.WaitOneShot(e.Ticks);
};
logo.ClearScreen += (s, e) => turtle.Clear();
logo.GoHome += (s, e) => turtle.Reset();
logo.ShowTurtle += (s, e) => turtle.ShowTurtle();
logo.HideTurtle += (s, e) => turtle.HideTurtle();
logo.PenErase += (s, e) => turtle.PenErase();
logo.PenNormal += (s, e) => turtle.PenNormal();
logo.SetX += (s, e) => turtle.SetX(e.X);
logo.SetY += (s, e) => turtle.SetY(e.Y);
logo.SetXY += (s, e) => turtle.SetXY(e.X, e.Y);
// New: LABEL text drawing event
logo.DrawLabel += (s, e) => turtle.DrawLabel(e.Text, e.X, e.Y, e.FontName, e.FontSize, e.FontStyle, e.R, e.G, e.B);
// New: SETFONT font setting event
logo.SetFont += (s, e) => turtle.SetFont(e.FontName, e.FontSize, e.Style);
logo.Execute(@"
REPEAT 4 [
FD 100
RT 90
]
");
// Use unified RenderToCanvas method (instead of turtle.Save directly)
var info = new SKImageInfo(800, 600);
using var surface = SKSurface.Create(info);
turtle.RenderToCanvas(surface.Canvas, 800, 600, 1.0f, drawTurtleIcon: true);
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var stream = File.OpenWrite("output.png");
data.SaveTo(stream);
}
The Turtle class exposes the following APIs:
SetCanvasSize(int width, int height)- set the canvas sizeShowTurtle()/HideTurtle()- toggle turtle visibilitySave(string fileName)- save the rendered image to a fileGetLineSegments()- get the list of drawn line segments for custom renderingGetTurtleState()- get the current turtle position, angle, and visibilityRenderToCanvas(SKCanvas canvas, float canvasWidth, float canvasHeight, float scale, bool drawTurtleIcon)— unified rendering method that draws all elements (lines, dots, arcs, labels, turtle icon) onto anySKCanvas. Shared by both the headless PNG export path (CreateRenderSurface) and the Avalonia GUI control (TurtleCanvas), avoiding duplicated rendering logic. Thescaleparameter controls world-coordinate-to-pixel ratio (1.0 = 1:1). SeeTurtleCanvas.csfor GUI integration examples.DelayMilliseconds- control the delay between drawing steps. The delay only applies to drawing operations (PENDOWN / PENERASE). Non-drawing moves (e.g.SetX/SetY/SetXY/MoveForwardwhile PENUP) are instantaneous, so repositioning the turtle with the pen up does not stall the program.Delay behavior:
0ms: instantaneous drawing, no sleep overhead.1–19ms: batches multiple segments into a single sleep to amortize OS scheduling overhead. For example, at 1 ms delay, 20 segments are drawn before a single 20 ms sleep; at 10 ms, 2 segments before a 20 ms sleep.20ms or higher: one segment per sleep, matching the requested delay exactly.
On Windows 10 (1803+) and above, .NET 10.0/.NET Framework 4.8, delays use QuickTickLib for high-precision timing via IO Completion Ports. On older Windows versions or unsupported platforms, it falls back to
Thread.Sleep.
Key Features
AOTLogoSharp provides the following commands and features:
- Basic Pen Commands
- PENDOWN/PD
- PENUP/PU
- SETPENCOLOR / SETPC / PC — set the pen (stroke) color. Accepts:
- integer 0–15 (standard Logo color index)
[R G B]list (three components, 0–255)[R G B A]list (four components with alpha)
- SETBG / SETBACKGROUND — set the canvas background color. Accepts:
- integer 0–15 (standard Logo color index)
[R G B]list (three components, 0–255)[R G B A]list (four components with alpha; clears to that RGBA)
- SETPENSIZE (non-negative real only, such as SETPENSIZE [2])
- PENERASE/PE
- PENNORMAL/PN
- SETFLOODCOLOR — set the flood-fill color and used by the
FILLcommand. Accepts:- integer 0–15 (standard Logo color index)
[R G B]list (three components, 0–255)[R G B A]list (four components with alpha)
- FILL — flood-fill the enclosed region at the turtle's current position with the current flood color. Fires
SetFloodColorandFillevents. See FILL and SETFLOODCOLOR.
- Basic Drawing Commands (real only)
- LEFT/LT (anticlockwise, real only)
- RIGHT/RT (clockwise, real only)
- FORWARD/FD (real only; FD 0 draws a dot via point set, batch-rendered for fractal performance)
- BACKWARD/BK/BACK (real only)
- DRAW/CLS/CLEARSCR/CLEARSCREEN/CS
- Turtle Control Commands
- HOME
- SHOWTURTLE/ST
- HIDETURTLE/HT
- SETX/SETY/SETXY (real only)
- SETCOMPLEX z — move the turtle to coordinates
(Re(z), Im(z))in a single built-in call. Semantically equivalent toSETXY (Re z) (Im z), but the implementation goes directly through the complex value rather than callingMAKEtwice to extract the real and imaginary parts. Use it whenever the position is naturally expressed as a complex number (e.g. fractal/IFS-style drawing). Example:SETCOMPLEX 3 + 4imoves the turtle to(3, 4). Accepts any expression that evaluates to a complex value, including:z + 0.5,3i * 2, etc. - SETPOLAR ρ θ — move the turtle to polar coordinates (ρ, θ), where ρ is the radius and θ is the angle in radians. Internally computes
x = ρ cos θ,y = ρ sin θand callsSETXY. Example:SETPOLAR 100 PImoves the turtle to(-100, 0),SETPOLAR 100 PI / 2moves to(0, 100). This simplifies drawing circles and polar-curve patterns (e.g. roses, spirals). - SETH/SETHEADING (real only; Logo semantics: 0° = north, clockwise; set
logo.HeadingMode = "standard"for 0° = east, counter-clockwise)
- Flow Control Commands
- REPEAT count (non-negative real, auto-rounded to integer) and RepCount
- FOR [varname start limit step?] [body] — flexible iteration. The control list specifies a variable name, start value, limit value, and optional step size. On each iteration, the variable is updated and the body executes.
- Real FOR: when
start,limit, andstep(if given) are all real, the loop runs whilesign(current - limit) != sign(step). If no step is given, it defaults to1or-1and the body always runs at least once (do-while semantics). The expression(END - START) / STEPmust be a non-negative real number; otherwise aRuntimeExceptionis thrown (e.g.FOR [T 0 1 -1]is rejected). Example:FOR [I 2 7 1.5] [PRINT :I]outputs2,3.5,5,6.5. - Complex FOR (explicit step only): when any of
start,limit, orstepis complex, the loop computes(END - START) / STEP— which must be a non-negative real integer n — and iteratesi = 0..n, assigningcurrent = start + i * stepat each iteration. The real and imaginary parts increase/decrease together. Example:FOR [T 0 3+3i 1+1i] [PRINT :T]outputs0,1+i,2+2i,3+3i. Without step, both start and limit must be real (FMSLogo semantics).
- Real FOR: when
- IF condition [body] — single-branch conditional. Condition supports complex: non-zero (including non-zero complex) is true. Example:
IF :x > 0 [PRINT [positive]],IF :z == 3 + 4i [PRINT [match]]. - IF ... ELSEIF ... ELSE — multi-branch conditional. After
IF cond [body], any number ofELSEIF cond [body]clauses can follow, optionally ending withELSE [body]. Only the first branch whose condition is true executes. Example:IF :x > 0 [PRINT [positive]] ELSEIF :x = 0 [PRINT [zero]] ELSE [PRINT [negative]] - IF ... ELSE — dual-branch conditional (shorthand without ELSEIF). Example:
IF :x > 0 [PRINT [positive]] ELSE [PRINT [non-positive]]. - IFELSE condition [true-body] [false-body] — legacy dual-branch conditional (FMSLogo-compatible). Executes the first bracketed block when the condition is true, otherwise the second. Example:
IFELSE :x > 0 [PRINT [positive]] [PRINT [non-positive]]. - WHILE (condition supports complex)
- DELAY (non-negative real only)
- WAIT (non-negative real only)
- STOP (exits the innermost running procedure)
- LOCAL "name or [name1 name2 ...] — declare one or more variables as local to the current procedure scope. Local variables shadow global variables and are automatically released when the procedure returns.
LOCAL "Xdeclares a single variable;LOCAL [P Q]declares multiple. See LOCAL Command. - OUTPUT/OP/RETURN — return a value from the current procedure and exit immediately. Return types are the same as those supported by MAKE (Scalar/Complex, List literal, etc. — see MAKE Command and Types). The caller obtains the value via
MAKE "var {PROC args}or expression context. Difference from STOP:STOPonly exits the procedure without returning a value;OUTPUTexits and returns a value. Example:TO GETRED OUTPUT [255 0 0] END MAKE "c {GETRED} SETPENCOLOR :c ; equivalent to SETPENCOLOR [255 0 0]
- Geometric Arc Commands (real only, radius non-negative)
- ARC angle radius — draws a circular arc centered at the turtle position with given angle (degrees) and radius. Positive angle = clockwise, negative angle = counter-clockwise. The arc starts from the direction opposite to the turtle heading (heading + 180°). The turtle does not move. Supports geometric erasure (PE/PN): drawing the same arc a second time in PE mode erases the overlapping portion.
- ARC2 angle radius — draws a circular arc with the center positioned to the right of the turtle heading at distance radius, starting along the heading direction. Positive angle = clockwise, negative angle = counter-clockwise. The turtle moves along the arc trajectory. Also supports geometric erasure.
- CIRCLE radius — draws a full 360° circle centered at the turtle position. Equivalent to
ARC 360 radius.
- Text Output Commands
- PRINT — output text to the console/event (complex values formatted as
a+bi). See PRINT Command for details. - PRINT FORM number width decimals — format a number with fixed width and decimal places, right-aligned with leading spaces (complex: precision applies to both parts). See FORM Command.
- TYPE — output text without trailing newline. String literals stripped, lists space-joined no brackets. In
(...)with string literals and+,+acts as string joiner:TYPE ("A + "B + "C)→ABC. See PRINT / TYPE / SHOW Commands. - SHOW — output text like PRINT but lists displayed with brackets:
[1 2 3]. See PRINT / TYPE / SHOW Commands. - LABEL — draw text on the canvas at the turtle's current position (complex values formatted as
a+bi). See LABEL Command. Labels are always rendered left-to-right, unlike MSWLogo where text follows the turtle heading. - LABEL FORM number width decimals — draw a formatted number on the canvas. See FORM Command.
- SETFONT — set the font for subsequent
LABELcommands (font name, size, style). See SETFONT Command.
- PRINT — output text to the console/event (complex values formatted as
- Language Features
- MAKE Command (Variable Assignment)
- Basic form:
MAKE "name expression. Stores the result of the expression into a variable; the variable name must begin with". - see MAKE Command and Types.
- Basic form:
- Complex Number Support
- Imaginary literals: append
iafter a number, e.g.4i,2.5i,1i. The imaginary unit must be written as1i(standaloneiis treated as a variable name to avoid conflicts withFOR [I 1 10]etc.). - Complex numbers are formed as real + imaginary:
MAKE "z 3 + 4i(i.e. 3+4i). - Real (double) values auto-promote to Complex when combined with complex operands; results with imaginary part < 1×10⁻¹⁴ auto-demote to double.
- Passing complex to real-only commands/operators throws a
RuntimeExceptionwith precise[line:column]position.
- Imaginary literals: append
- Expressions
- Arithmetic (supports complex):
+,-,*,/,^ - Arithmetic (real only):
%(remainder) - Comparison (supports complex):
==,=,<>,!= - Comparison (real only):
<,>,<=,>= - Comparison predicate functions (real only, return 1.0 = TRUE / 0.0 = FALSE):
- LESSP num1 num2 —
num1 < num2, equivalent to<operator. Alias: LESS?. - LESSEQUALP num1 num2 —
num1 <= num2, equivalent to<=operator. Alias: LESSEQUAL?. - GREATERP num1 num2 —
num1 > num2, equivalent to>operator. Alias: GREATER?. - GREATEREQUALP num1 num2 —
num1 >= num2, equivalent to>=operator. Alias: GREATEREQUAL?. - BEFOREP word1 word2 — test whether word1 comes before word2 in dictionary order (case-insensitive; numbers compared as strings, e.g.
BEFOREP 9 10returns FALSE because'9' > '1'; Chinese: 零~十 sorted by numeric value, otherwise by pinyin initial letter, falling back to Unicode codepoint order when pinyin initials match or characters are outside the extended CJK range). Alias: BEFORE?. - CASEIGNOREDP — return whether case-insensitivity is active (AOTLogoSharp always returns
1.0).
- LESSP num1 num2 —
- String/List comparison:
=,==,<>,!=now support string content comparison and list recursive structural comparison (used in IF/IFELSE/ELSEIF/WHILE). Different types are never equal. - Logic (supports complex): NOT (unary, multi-arg throws error)
- Logic (real only, variadic/short-circuit):
- AND a b c… = a AND b AND c… (cascaded AND)
- OR a b c… = a OR b OR c… (cascaded OR)
- NAND a b c… = NOT (a AND b AND c…) (cascade AND then negate)
- NOR a b c… = NOT (a OR b OR c…) (cascade OR then negate)
- XOR a b c… = a XOR b XOR c… (cascaded XOR)
- XNOR a b c… = NOT (a XOR b XOR c…) (cascade XOR then negate)
- Braced expressions:
{expr}(inline computation, not just named functions)
- Arithmetic (supports complex):
- Procedures
- Function Calls
- SQRT (supports complex; negative reals auto-return complex)
- RANDOM (real only)
- Trigonometric (degrees, real-only): SIN/COS/TAN/COT/SEC/CSC
- Inverse trig (returns degrees, real-only): ASIN/ACOS/ATAN/ACOT/ASEC/ACSC, aliases: ARCSIN/ARCCOS/ARCTAN/ARCCOT/ARCSEC/ARCCSC
- Trigonometric (radians, supports complex, FMSLogo-compatible): RADSIN/RADCOS/RADTAN/RADCOT/RADSEC/RADCSC
- Inverse trig (returns radians, supports complex, FMSLogo-compatible): RADASIN/RADACOS/RADATAN/RADACOT/RADASEC/RADACSC, aliases: RADARCSIN/RADARCCOS/RADARCTAN/RADARCCOT/RADARCSEC/RADARCCSC
- Hyperbolic (radians, supports complex): SINH/COSH/TANH/COTH/SECH/CSCH
- Inverse hyperbolic (returns radians, supports complex): ASINH/ACOSH/ATANH/ACOTH/ASECH/ACSCH, aliases: ARCSINH/ARCCOSH/ARCTANH/ARCCOTH/ARCSECH/ARCCSCH
- Special functions (supports real and complex): TGAMMA (Gamma)/LGAMMA (ln|Γ|)/ERF/ERFC/ERFCX
- ERFINV (supports real and complex) — inverse error function
erfinv(x), satisfyingerf(erfinv(x)) = x. Real path uses Giles (2011) double-precision three-segment polynomial + Newton polishing, full-range error ≤ 1 ulp. Complex path uses Newton iteration. - ZETA (supports real and complex) — Riemann zeta function
ζ(s). Returns +∞ ats = 1; returns 0 at negative even integers (trivial zeros); uses Euler-Maclaurin summation forRe(s) > 0; uses functional equation forRe(s) < 0. - RELU x (real only) — activation function ReLU:
relu(x) = max(0, x). Complex input throws exception. - LEAKYRELU x alpha (real only) — activation function Leaky ReLU:
leakyrelu(x, α) = x > 0 ? x : α·x. Second argument is the negative slope (commonly 0.01). Complex input throws exception. Example:{LEAKYRELU -2 0.01}→-0.02. - SIGMOID (supports real and complex) — activation function Sigmoid:
σ(x) = 1 / (1 + e^(-x)). Real path is numerically stable (split positive/negative to avoid overflow). - SINC (supports real and complex) — normalized Sinc function:
sinc(x) = sin(πx) / (πx),sinc(0) = 1. Note this is the normalized version (parameter isπx). - ABS (supports complex, returns magnitude; alias: MAGNITUDE)
- POWER (supports complex base and/or exponent)
- EXP (supports complex input)
- LOG (natural, supports complex input, negative reals auto-return complex) / LOG10 (supports complex input), alias: LN
- REAL z — extract real part. Returns itself for real numbers. Alias: RE.
- IMAGINARY z — extract imaginary part coefficient. Returns 0 for real numbers. Alias: IM.
- PHASE z — complex argument (returns radians). Returns 0 for positive reals, π for negative reals. Alias: ARG.
- CONJUGATE z — complex conjugate. Returns itself for real numbers. Alias: CONJ.
- FMA z1,z2,z3 — complex fused multiply-add z1×z2+z3, three comma-separated arguments, hardware accelerated. Example:
{FMA 3+4i, 2, 5}→11+8i. - FDA z1,z2,z3 — complex fused divide-add (z1/z2)+z3, three comma-separated arguments, hardware accelerated (Vector256/V128/scalar three-tier switch-case dispatch). Example:
{FDA 10,2,3}→8. - SE/WORD/LIST (multi-arg via
(SE 1 2 3)) - REVERSE thing — reverse a string or list. Strings are reversed by Unicode grapheme cluster (correctly handles emoji, ZWJ sequences, combining characters); lists reverse only the outermost level. Examples:
{REVERSE "hello}→"olleh,{REVERSE [1 2 3]}→[3 2 1]. - FPUT thing list — prepend thing to the front of list, returning a new list. Example:
{FPUT 1 [2 3 4]}→[1 2 3 4]. - LPUT thing list — append thing to the end of list, returning a new list. Example:
{LPUT 4 [1 2 3]}→[1 2 3 4]. - SORT sequence or (SORT sequence predicate**)** — sort. Single-arg default: numbers by
<, non-numbers by dictionary order. Predicate can be a procedure name (e.g."GREATERP") or an explicit slot template (e.g.[LESSP LAST ?1 LAST ?2]). Sorting produces a new sequence; the original is unchanged. Examples:{SORT [3 1 2]}→[1 2 3],{SORT "hello}→"ehllo. - FIRST thing — return the first grapheme cluster of a string or the first member of a list. Examples:
{FIRST "hello}→"h,{FIRST [1 2 3]}→1. - LAST thing — return the last grapheme cluster of a string or the last member of a list. Examples:
{LAST "hello}→"o,{LAST [1 2 3]}→3. - ITEM index thing — 1-based index access. Examples:
{ITEM 2 "hello}→"e,{ITEM 2 [10 20 30]}→20. - SETITEM index list value — 1-based index modification of a list element (second argument must be a variable reference). Example:
SETITEM 2 :mylist 99sets the 2nd element of:mylistto 99. - PERM n k (supports complex) — permutation count
P(n, k) = n! / (n-k)!. Integer inputs use exact multiplicative counting; non-integer inputs uselgammaso the formula extends smoothly to non-integer n / k; complex inputs use complexlgamma. Examples:{PERM 5 3}→60,{PERM 10 3}→720. - COMB n k (supports complex) — combination count
C(n, k) = n! / (k! (n-k)!). Integer inputs use the exact closed form viaPERM; non-integer inputs uselgamma; complex inputs use complexlgamma. Examples:{COMB 10 4}→210,{COMB 5.5 2.5}→14.4375. - LAMBERTW k z — Lambert W function (product logarithm), satisfying
z = W(z) · e^{W(z)}. k is the branch index (0 = principal branch, -1 = lower branch; other integer k supported for complex z). Real inputs use Halley's method (~1e-15 precision). Complex inputs route through .NET's optimized Complex routines. Aliases: W, PRODUCTLOG. Examples:{LAMBERTW 0 1}→ Ω ≈ 0.5671433 (Omega constant),{LAMBERTW 0 e}→ 1,{W 0 1+i},{PRODUCTLOG -1 -0.1}. - FORM number width decimals — format a number (or complex) to fixed string width with decimal places (FMSLogo-compatible). Complex: precision applies to both parts, width is total string length + padding. Can be used standalone as
{FORM 3.14 6 2}, after PRINT/LABEL asPRINT FORM 3.14 6 2, or as{FORM :var 4 1}. - SIGN x (real only) — return -1 (negative), 0 (zero), or 1 (positive).
- ROUND x (real only) — round to the nearest integer (midpoint rounding to even).
- FLOOR x (real only) — round down to the largest integer ≤ x.
- CEIL x (real only) — round up to the smallest integer ≥ x.
- TRUNC x (real only) — truncate toward zero, discarding the fractional part.
- REMAINDER dividend divisor (real only) — return the remainder of dividend / divisor. The sign of the result matches the dividend (e.g.
{REMAINDER -7 3}→-1). Alias: REM. Equivalent infix operator:%(e.g.17 % 5→2). - MAX a b ... (real only, variadic) — return the largest value among all arguments. Supports 1 to N arguments: single-arg returns itself, two-arg uses
Math.Max, three or more usesLinq.Max. Examples:{MAX 3 7}→7,{MAX 1 5 3}→5,{MAX {ABS -4} {ABS 2}}→4. - MIN a b ... (real only, variadic) — return the smallest value among all arguments. Supports 1 to N arguments: single-arg returns itself, two-arg uses
Math.Min, three or more usesLinq.Min. Examples:{MIN 3 7}→3,{MIN 1 5 3}→1,{MIN {ABS -4} {ABS 2}}→2. - GCD a b ... (integer only, variadic) — return the greatest common divisor of all arguments via the Euclidean algorithm. Supports 1 to N integer arguments; result is always non-negative. Single-arg returns the absolute value of itself. Only integers supported: passing complex numbers or non-integer real values (e.g.
3.5) throwsRuntimeException. Negative inputs are allowed — their absolute values are used. Examples:{GCD 12 8}→4,{GCD 60 48 36}→12,{GCD -15 25}→5,{GCD 7}→7,{GCD 0 5}→5. - LCM a b ... (integer only, variadic) — return the least common multiple of all arguments. Supports 1 to N integer arguments; result is always non-negative. Single-arg returns the absolute value of itself. Only integers supported: passing complex numbers or non-integer real values throws
RuntimeException. Negative inputs are allowed — their absolute values are used. If any argument is 0, the result is 0 (by mathematical convention). Internally computes|a / gcd(a,b) * b|. Examples:{LCM 4 6}→12,{LCM 3 4 5}→60,{LCM -6 8}→24,{LCM 7}→7,{LCM 0 5}→0. - STRING expr — convert to string: number →
Scalar.Format(), string → as-is, list → space-joined (no brackets). See Type Conversion Functions. - INT expr — convert to integer (long): string parsed as double then truncated toward zero; number truncated toward zero. Not a TRUNC alias; does not support complex/list. See Type Conversion Functions.
- DOUBLE expr — convert to real (double): string parsed as double; complex → real part (imaginary discarded); does not support list. See Type Conversion Functions.
- COMPLEX expr — convert to complex: string parsed via
ParseComplexString(formats:3,3.5,4i,-2i,3+4i,3-4i,1+i,1-i,i,-i,1e2+3i); number → as-is. See Type Conversion Functions.
- Inline Comments
- Built-in Constants
- PI
- E
- MAKE Command (Variable Assignment)
Single-arg vs. multi-arg function parameter syntax:
- Single-arg functions (e.g.
{EXP expr},{SQRT expr},{LGAMMA expr}): the expression after the function name may contain binary operators, e.g.{EXP :a + :b}is equivalent toEXP(a + b).- Multi-arg functions (e.g.
{POWER base exp},{PERM n k},{COMB n k},{MAX a b}): arguments are space-separated. If an argument itself contains a binary operator or unary minus, it must be wrapped in parentheses, e.g.{POWER 2 (-:X)}meansPOWER(2, -X). Negative numeric literals (e.g.-1) do not need parentheses — the lexer recognizes them as a complete number.
Important: All function calls (both built-in and TO...END user-defined procedures) MUST be wrapped in curly braces
{ }. For example,{SQRT 2},{tpdf 1 5}. Without curly braces, the parser cannot disambiguate and will report "Expected expression". User-defined procedures are no exception: define withTO procname :a :b ... END, then call with{procname 1 2}. When calling user-defined procedures inside SETXY or other commands, always use curly braces:SETXY -100 {catenary 1 2} * 40.
API Breaking Changes (v1.3 → v1.4)
ConstantExpressionNode(publicclass):Valueproperty type changed fromfloattodouble, constructor signature updated accordingly.- All numeric computations now use double precision (aligns with UCBLogo/MSWLogo spec), no more single-precision truncation.
- License changed from MIT to BSD-3-Clause (see
THIRD-PARTY-NOTICE.txtfor NetTopologySuite, SkiaSharp, PolySharp, QuickTickLib licenses).
API Breaking Changes (v1.5 → v1.6)
- Complex number support:
Evaluation.Valuetype changed fromdoubletoScalar(a 24-byte value type struct). All internal numeric computations now useScalarinstead ofobject, eliminating boxing overhead. - Trampoline execution engine: User-defined procedure calls now use a heap-based trampoline loop instead of C# recursion.
MaxCallStackDepth(default 500) is now checked on the heap stack, so it can be set much higher without risking a realStackOverflowException. STOP/OUTPUT in nested blocks (REPEAT/WHILE/IF) still use exception control flow internally but are caught at the trampoline boundary. - New target framework: Added
netstandard2.1TFM.System.Memoryis now referenced only fornetstandard2.0/net48.
Limitations
- Multi-line source is supported inside list literals (
[ ... ]), e.g. aREPEATbody or aPRINTlist can span multiple lines. What is not supported is splitting a command from its argument across lines, e.g.FDon one line and100on the next. - Logo commands other than the ones listed above are not supported. More will be added in future
- Function calls (both built-in and user-defined procedures) MUST be wrapped in curly braces, e.g.
{SQRT 2}or{tpdf 1 5} PRINT "textreads the literal up to the next whitespace or delimiter (parenthesis / bracket). To include spaces inside the printed text, use a list literal:PRINT [Hello World]- Quoted identifiers (
"name) and variable/keyword matching are both case-insensitive, but the text inside aPRINT "textliteral is preserved verbatim in its original casing. - LABEL text is always rendered left-to-right on the canvas, regardless of the turtle heading. This differs from MSWLogo where label text rotates with the turtle.
Headless Mode / CI Batch Image Generation
LogoSharp.Main doubles as a headless batch renderer. When invoked with .logo file paths (supports * and ? wildcards), it skips the Avalonia GUI and runs every file through the full Logo → Turtle → PNG pipeline automatically:
# Render a single file
LogoSharp.Main.exe samples/house.logo
# Render all samples in batch (wildcards supported)
LogoSharp.Main.exe samples/*.logo
# Render specific subsets
LogoSharp.Main.exe samples/house*.logo samples/test-*.logo
How it works (see HeadlessMode.cs):
- Lists all matched
.logofiles sorted by name. - For each file, creates a fresh
Turtleinstance (naturally isolated, no cross-file state leak). - Wires all
Logoevents (FD, BK, RT, LT, SETPC, LABEL, SETFONT, etc.) to the turtle — exactly the same subscriptions as the Avalonia GUI. - Calls
logo.Execute(sourceCode)thenturtle.CreateRenderSurface()→ encodes to PNG. - PNGs are saved to
headless-output/<yyyyMMdd-HHmmss>/under the exe directory, one per.logofile. - Prints per-file
[OK/FAIL]lines to stdout and aggregate timing stats at the end. Exit code is 0 only when all files succeed; non-zero if any file fails (parse error, runtime error, or I/O error).
CI integration: The project's own GitHub Actions workflow publishes LogoSharp.Main as a Native AOT executable and runs the full samples/*.logo suite as a CI step — validating that every sample still renders correctly on every push and PR.
Memory safety: A MemoryGuard watchdog (default 1024 MB threshold) monitors the process during headless runs, killing it immediately if a parser infinite-loop or runaway allocation is detected.
WinForm Samples (Deprecated)
The WinForm sample projects located at src-winform/LogoSharp.Drawing/ and src-winform/LogoSharp.Main/ are no longer maintained. They are preserved for historical reference only. New development should use the Avalonia UI sample at src/LogoSharp.Main/ instead.
License
BSD-3-Clause. See LICENSE. Third-party licenses are listed in THIRD-PARTY-NOTICE.txt.
Donate
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 is compatible. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.8
- Microsoft.Bcl.HashCode (>= 6.0.0)
- Shim.System.Text.Rune (>= 6.0.2)
- System.Memory (>= 4.6.3)
- System.Runtime.CompilerServices.Unsafe (>= 6.1.2)
- TinyPinyin.NetFramework (>= 1.0.2.2)
-
.NETStandard 2.0
- Microsoft.Bcl.HashCode (>= 6.0.0)
- Shim.System.Text.Rune (>= 6.0.2)
- System.Memory (>= 4.6.3)
- System.Runtime.CompilerServices.Unsafe (>= 6.1.2)
- TinyPinyin.NetFramework (>= 1.0.2.2)
-
.NETStandard 2.1
- Shim.System.Text.Rune (>= 6.0.2)
- System.Runtime.CompilerServices.Unsafe (>= 6.1.2)
- ToolGood.Words.FirstPinyin (>= 3.1.0.4)
-
net10.0
- ToolGood.Words.FirstPinyin (>= 3.1.0.4)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on AOTLogoSharp:
| Package | Downloads |
|---|---|
|
AOTLogoSharp.Drawing
LogoSharp drawing layer based on SkiaSharp, AOT and trimming friendly. Forked and adapted by 4Darmygeometry (https://github.com/4Darmygeometry/AOTLogoSharp). |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0 | 68 | 7/30/2026 |
| 2.2.2 | 134 | 7/21/2026 |
| 2.2.1 | 129 | 7/21/2026 |
| 2.2.0 | 137 | 7/17/2026 |
| 2.1.0 | 148 | 7/15/2026 |
| 2.0.1 | 154 | 7/11/2026 |
| 2.0.0 | 139 | 7/10/2026 |
| 1.9.2 | 148 | 7/6/2026 |
| 1.9.1 | 149 | 7/5/2026 |
| 1.9.0 | 133 | 7/4/2026 |
| 1.8.0 | 156 | 6/26/2026 |
| 1.7.1 | 150 | 6/22/2026 |
| 1.7.0 | 153 | 6/21/2026 |
| 1.6.2 | 153 | 6/20/2026 |
| 1.6.1 | 146 | 6/20/2026 |
| 1.6.0 | 158 | 6/20/2026 |
| 1.5.5 | 162 | 6/19/2026 |
| 1.5.4 | 146 | 6/19/2026 |
| 1.5.3 | 150 | 6/19/2026 |
| 1.5.2 | 149 | 6/19/2026 |