AOTLogoSharp.Drawing 2.0.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package AOTLogoSharp.Drawing --version 2.0.1
                    
NuGet\Install-Package AOTLogoSharp.Drawing -Version 2.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AOTLogoSharp.Drawing" Version="2.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AOTLogoSharp.Drawing" Version="2.0.1" />
                    
Directory.Packages.props
<PackageReference Include="AOTLogoSharp.Drawing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AOTLogoSharp.Drawing --version 2.0.1
                    
#r "nuget: AOTLogoSharp.Drawing, 2.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AOTLogoSharp.Drawing@2.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AOTLogoSharp.Drawing&version=2.0.1
                    
Install as a Cake Addin
#tool nuget:?package=AOTLogoSharp.Drawing&version=2.0.1
                    
Install as a Cake Tool

AOTLogoSharp

Logo programming language for managed world.

中文文档

Build status NuGet Badge

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.

  1. Install AOTLogoSharp NuGet Package, for example:
Install-Package AOTLogoSharp -Version 2.0.1
  1. 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");
}
  1. 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

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 use Scalar.Format() (e.g. 3+4i, 3.5, 5).
  • TYPE arg — identical to PRINT but no trailing newline. Multiple arguments (via (WORD ...) or mixed concat) are concatenated without separators.
  • SHOW arg — identical to PRINT but 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) outputs 3.

Example:

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.

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.

Key rules:

  • The first argument is always a variable name ("name), never an expression.
  • MAKE sets 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 to A do not affect B.
  • Function return values can be assigned: MAKE "FN {GETNUM} or MAKE "FL {GETLIST}.
  • Numbers, strings, and lists are not implicitly convertible. Use the Type Conversion Functions below.

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. Note that Logo string literals cannot contain + (it is a lexer delimiter), so "3+4i is tokenized as "3 + 4i. To pass a +-containing complex string, use a variable or construct it via WORD.

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 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.

Notes:

  • LOCAL "X declares a single local variable; LOCAL [P Q] declares multiple.
  • Local variables shadow same-named global variables within the procedure.
  • Declared locals are initialized to undefined until MAKE assigns 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:

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 YaHei only captures Microsoft; you must write [Microsoft YaHei]
    • Empty brackets [] reset to the default CJK font: SETFONT [] 0 0 restores the default 14px CJK font
  • 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:

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.14
  • LABEL FORM :x 4 1 → draws e.g. 1.2 or -0.6 on 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 (default 10000) — caps the count of a single REPEAT and the per-step iterations of a WHILE loop. Also applies to FOR: the expected iteration count is pre-computed from start/limit/step and rejected if it exceeds this limit. Each nested FOR is independently capped, so the actual total work of nested loops is bounded by MaxRepeatIterations^depth. For high-count non-nested loops, prefer REPEAT over FOR since REPEAT is simpler and has no control variable overhead. Exceeding the limit throws RuntimeException with a StackOverflowException prefix.
  • MaxCallStackDepth (default 500) — 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.

  1. Install AOTLogoSharp.Drawing NuGet Package, for example:
Install-Package AOTLogoSharp.Drawing -Version 2.0.1
  1. 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 size

  • ShowTurtle() / HideTurtle() - toggle turtle visibility

  • Save(string fileName) - save the rendered image to a file

  • GetLineSegments() - get the list of drawn line segments for custom rendering

  • GetTurtleState() - get the current turtle position, angle, and visibility

  • RenderToCanvas(SKCanvas canvas, float canvasWidth, float canvasHeight, float scale, bool drawTurtleIcon) — unified rendering method that draws all elements (lines, dots, arcs, labels, turtle icon) onto any SKCanvas. Shared by both the headless PNG export path (CreateRenderSurface) and the Avalonia GUI control (TurtleCanvas), avoiding duplicated rendering logic. The scale parameter controls world-coordinate-to-pixel ratio (1.0 = 1:1). See TurtleCanvas.cs for 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/MoveForward while PENUP) are instantaneous, so repositioning the turtle with the pen up does not stall the program.

    Delay behavior:

    • 0 ms: instantaneous drawing, no sleep overhead.
    • 119 ms: 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.
    • 20 ms 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 (integer 0–15, or RGB list [0–255] non-negative real only, auto-rounded to integer)
    • 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)
    • PENERASE/PE
    • PENNORMAL/PN
    • SETFLOODCOLOR — set the flood-fill color (RGB or [R G B] list). Used by the FILL command.
    • FILL — flood-fill the enclosed region at the turtle's current position with the current flood color. Fires SetFloodColor and Fill events. 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 to SETXY (Re z) (Im z), but the implementation goes directly through the complex value rather than calling MAKE twice 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 + 4i moves 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 calls SETXY. Example: SETPOLAR 100 PI moves the turtle to (-100, 0), SETPOLAR 100 PI / 2 moves 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, and step (if given) are all real, the loop runs while sign(current - limit) != sign(step). If no step is given, it defaults to 1 or -1 and the body always runs at least once (do-while semantics). The expression (END - START) / STEP must be a non-negative real number; otherwise a RuntimeException is thrown (e.g. FOR [T 0 1 -1] is rejected). Example: FOR [I 2 7 1.5] [PRINT :I] outputs 2, 3.5, 5, 6.5.
      • Complex FOR (explicit step only): when any of start, limit, or step is complex, the loop computes (END - START) / STEP — which must be a non-negative real integer n — and iterates i = 0..n, assigning current = start + i * step at each iteration. The real and imaginary parts increase/decrease together. Example: FOR [T 0 3+3i 1+1i] [PRINT :T] outputs 0, 1+i, 2+2i, 3+3i. Without step, both start and limit must be real (FMSLogo semantics).
    • 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 of ELSEIF cond [body] clauses can follow, optionally ending with ELSE [body]. Only the first branch whose condition is true executes. Example:
    • 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 "X declares a single variable; LOCAL [P Q] declares multiple. See LOCAL Command.
    • OUTPUT/OP/RETURN — return a value from the current procedure and exit immediately. Supports three return forms:
      • Scalar / Complex: OUTPUT 42, OUTPUT :x + 1, OUTPUT :z (complex). Returns a single Scalar value; the caller obtains it via {PROC args}.
      • List literal [expr*]: OUTPUT [255 0 0], OUTPUT [:R :G :B]. Returns a List<double>; the caller stores it via MAKE "var {PROC args} and can then use it directly with SETPENCOLOR :var etc. Example:
      • Difference from STOP: STOP only exits the procedure without returning a value; OUTPUT exits and returns a value, which the caller obtains via MAKE "var {PROC args} or expression context.
  • 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 LABEL commands (font name, size, style). See SETFONT Command.
  • 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 ".
      • Supported right-hand side forms:
        • Scalar / Complex: MAKE "x 42, MAKE "z 3 + 4i
        • List literal [expr*]: MAKE "rgb [255 0 0]. Each element in the list is evaluated as double and stored as List<double>, directly usable with SETPENCOLOR :rgb, SETBG :rgb, and other commands that accept list arguments.
        • Function call {PROC args}: MAKE "color {HOTCOLOR :iter :max}. When a function (user-defined procedure) returns a list via OUTPUT [list], the variable stores the full List<double>.
        • Expression: MAKE "sum :a + :b, MAKE "half :W / 2
      • Variable reference: Use :name to read a variable's value, e.g. :x, :rgb, :sum.
      • Variables as lists: When a variable holds a List<double>, referencing it automatically expands to list arguments. For example, after MAKE "c {GETRED}, SETPENCOLOR :c is equivalent to SETPENCOLOR [255 0 0].
    • Complex Number Support
      • Imaginary literals: append i after a number, e.g. 4i, 2.5i, 1i. The imaginary unit must be written as 1i (standalone i is treated as a variable name to avoid conflicts with FOR [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 RuntimeException with precise [line:column] position.
    • Expressions
      • Arithmetic (supports complex): +, -, *, /, ^
      • Arithmetic (real only): % (remainder)
      • Comparison (supports complex): ==, =, <>
      • Comparison (real only): <, >, <=, >=
      • Logic (supports complex): NOT
      • Logic (real only): AND, OR
      • Braced expressions: {expr} (inline computation, not just named functions)
    • 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: TGAMMA (Gamma)/LGAMMA (ln|Γ|)/ERF/ERFC/ERFCX. Note: TGAMMA / LGAMMA / ERF / ERFC / ERFCX only support real input (passing complex values throws RuntimeException)
      • 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.
      • SE/WORD/LIST (multi-arg via (SE 1 2 3))
      • PERM n k — permutation count P(n, k) = n! / (n-k)!. Integer inputs use exact multiplicative counting; non-integer inputs use lgamma so the formula extends smoothly to non-integer n / k. Examples: {PERM 5 3}60, {PERM 10 3}720.
      • COMB n k — combination count C(n, k) = n! / (k! (n-k)!). Integer inputs use the exact closed form via PERM; non-integer inputs use lgamma so the formula extends smoothly to non-integer n / k. 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 as PRINT 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 % 52).
      • 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 uses Linq.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 uses Linq.Min. Examples: {MIN 3 7}3, {MIN 1 5 3}1, {MIN {ABS -4} {ABS 2}}2.
      • 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

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 to EXP(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)} means POWER(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 with TO 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)

  1. ConstantExpressionNode (public class): Value property type changed from float to double, constructor signature updated accordingly.
  2. All numeric computations now use double precision (aligns with UCBLogo/MSWLogo spec), no more single-precision truncation.
  3. License changed from MIT to BSD-3-Clause (see THIRD-PARTY-NOTICE.txt for NetTopologySuite, SkiaSharp, PolySharp, QuickTickLib licenses).

API Breaking Changes (v1.5 → v1.6)

  1. Complex number support: Evaluation.Value type changed from double to Scalar (a 24-byte value type struct). All internal numeric computations now use Scalar instead of object, eliminating boxing overhead.
  2. 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 real StackOverflowException. STOP/OUTPUT in nested blocks (REPEAT/WHILE/IF) still use exception control flow internally but are caught at the trampoline boundary.
  3. New target framework: Added netstandard2.1 TFM. System.Memory is now referenced only for netstandard2.0 / net48.

Limitations

  • Code editing doesn't support multi-line format
  • 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 "text reads 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 a PRINT "text literal 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):

  1. Lists all matched .logo files sorted by name.
  2. For each file, creates a fresh Turtle instance (naturally isolated, no cross-file state leak).
  3. Wires all Logo events (FD, BK, RT, LT, SETPC, LABEL, SETFONT, etc.) to the turtle — exactly the same subscriptions as the Avalonia GUI.
  4. Calls logo.Execute(sourceCode) then turtle.CreateRenderSurface() → encodes to PNG.
  5. PNGs are saved to headless-output/<yyyyMMdd-HHmmss>/ under the exe directory, one per .logo file.
  6. 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.

pmBoq4e.md.jpg pmBTSDP.md.png

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 64 7/30/2026
2.2.2 92 7/21/2026
2.2.1 89 7/21/2026
2.2.0 92 7/17/2026
2.1.0 98 7/15/2026
2.0.1 101 7/11/2026
2.0.0 111 7/10/2026
1.9.2 99 7/6/2026
1.9.1 99 7/5/2026
1.9.0 96 7/4/2026
1.8.0 112 6/26/2026
1.7.1 112 6/22/2026
1.7.0 112 6/21/2026
1.6.2 109 6/20/2026
1.6.1 115 6/20/2026
1.6.0 108 6/20/2026
1.5.5 117 6/19/2026
1.5.4 105 6/19/2026
1.5.3 108 6/19/2026
1.5.2 109 6/19/2026
Loading failed