跳转至

AOTLogoSharp

面向托管世界的 Logo 编程语言。

Build status NuGet Badge

Logo 是一种编程语言,通过控制屏幕上的小海龟绘制出精美的图案,例如:

alt text

AOTLogoSharp 绘制的更多精美图案:

  • 盒子图

alt text

  • 美丽的花朵

alt text

  • 方形花朵

alt text

  • 蛛网

alt text

Logo 广泛用于儿童计算机教育,因为它简单有趣。AOTLogoSharp 是 Logo 编程语言的 .NET 实现,基于手写递归下降解析器。

为什么选择 AOTLogoSharp?

AOTLogoSharp 完全兼容 Native AOT裁剪(Trimming)。你可以将应用程序发布为体积小巧的单文件原生可执行文件,启动速度快,无 JIT 开销。这非常适合对部署体积和启动速度有要求的场景,例如命令行工具、教育应用或嵌入式绘图流水线。

程序集名称保持为 LogoSharp.dllLogoSharp.Drawing.dll,命名空间仍为 LogoSharp / LogoSharp.Drawing,因此引用原版 LogoSharp 的现有代码只需更换 NuGet 包即可迁移。

如何使用 AOTLogoSharp?

在基于 .NET Framework 4.8、.NET Standard 2.0 或 .NET 10 的 .NET 项目中使用 AOTLogoSharp 非常简单直接。这意味着你可以让 AOTLogoSharp 应用与 .NET Core 一起工作,从而具备跨平台能力。

  1. 安装 AOTLogoSharp NuGet 包,例如:
Install-Package AOTLogoSharp -Version 1.3.1
  1. 编写你的第一个应用:
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. 运行你的应用,你应该会在控制台看到消息 Forwarded 102 steps.

基本上,Logo 程序代码通过 logo 实例的 Execute 方法传入,AOTLogoSharp 会执行代码并触发事件。因此,订阅了特定事件的事件处理程序会在代码执行触发该订阅事件时被调用。

Logo 类提供 HeadingMode 属性来控制 SETH / SETHEADING 的角度语义: - "logo"(默认):0° = 正北,顺时针增加。符合标准 Logo 语言规范。 - "standard":0° = 正东,逆时针增加。符合数学极坐标惯例。

var logo = new Logo();
logo.HeadingMode = "standard"; // 切换到数学角度语义
logo.Execute("SETH 0");        // 此时指向正东而非正北

Logo 类还提供 WaitMode 属性来控制 WAIT 命令的延时单位: - "logo"(默认):1 刻度 = 1/60 秒(约 16.67 毫秒),遵循传统 Logo 规范。 - "ms" / "standard":1 刻度 = 1 毫秒。

var logo = new Logo();
logo.WaitMode = "logo";   // WAIT 1 ≈ 16.67 ms
logo.Execute("WAIT 60");  // 等待约 1 秒

AOTLogoSharp 支持 PRINT 命令,提供三种形式:

  • PRINT "text — 输出字符串字面量(" 之后到下一个空白 / 分隔符之间的全部内容)。
  • PRINT 42 — 输出数值。
  • PRINT [word1 word2 ...] — 输出列表字面量,空格拼接为一个句子。

每次 PRINT 调用都会触发 Print 事件,类型为 EventHandler<PrintEventArgs>,渲染好的文本通过 Text 属性给出。字符串字面量内容保留用户输入的原始大小写——例如 PRINT "Hello 会原样输出 Hello,而关键字(如 PRINT 本身)的匹配仍然是大小写不敏感的。

using LogoSharp;

var logo = new Logo();
logo.Print += (s, e) => Console.WriteLine($"[print] {e.Text}");
logo.Execute(@"
    PRINT [Test OK]
    PRINT ""你好 世界
    PRINT 3.14
");

错误报告

运行时错误(例如调用未定义的过程)会带上精确的 [行:列] 源码位置信息。当一次 Execute 调用中出现多个错误时,引擎会把它们聚合到同一个 RuntimeException 中,Message 里每行一条错误,便于宿主 UI 把每条错误作为独立列表项展示。

STOP 命令与递归 / 循环上限

STOP 是内置关键字,立即退出当前正在执行的最内层过程。它不会触发任何事件——而是在引擎内部抛出一个 StopException,由过程调度器吞掉,宿主 UI 不会看到崩溃。在过程外使用 STOP 没有效果(异常找不到捕获边界,会被重新抛出为普通的 RuntimeException)。

引擎为失控的 Logo 程序提供两道语言级防线,挂在 Logo 实例的两个公开属性上:

  • MaxRepeatIterations(默认 10000)— 同时限制单条 REPEAT 的次数和 WHILE 循环的每步迭代次数。超过后会抛 RuntimeExceptionMessageStackOverflowException 前缀开头。
  • MaxCallStackDepth(默认 500)— 限制用户自定义过程调用的递归深度。
var logo = new Logo
{
    MaxRepeatIterations = 1000000,   // 允许长时间运行的绘图
    MaxCallStackDepth  = 2000          // 允许更深的递归
};
logo.Execute("REPEAT 1000000 [FD 1]");

对于语言级限制无法覆盖的极端情况(病态输入导致 AST 无界增长等),还提供进程级看门狗 LogoSharp.MemoryGuard:周期性采样当前进程的私有内存,越界后立即 Environment.Exit(1) 退出:

using LogoSharp;

// 启动 1 GiB 看门狗(默认:1024 MB 阈值,1000 ms 检测间隔)
MemoryGuard.Start(thresholdMB: 1024, checkIntervalMs: 1000);

// ... 执行 Logo 代码 ...

MemoryGuard.Stop();

有关如何使用 AOTLogoSharp 的更多信息,请参阅 LogoSharp.Drawing 项目。

使用 AOTLogoSharp.Drawing

AOTLogoSharp.Drawing 提供了 Turtle 类,用于将 Logo 程序渲染为图像。它也可以作为 AOTLogoSharp.Drawing NuGet 包使用。

  1. 安装 AOTLogoSharp.Drawing NuGet 包,例如:
Install-Package AOTLogoSharp.Drawing -Version 1.3.1
  1. 编写你的第一个应用:
using LogoSharp;
using LogoSharp.Drawing;
using SkiaSharp;

static void Main(string[] args)
{
    var turtle = new Turtle();
    var logo = new Logo();

    // 将 Logo 事件连接到 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));
        // 关键:UI 操作必须在 UI 线程执行
        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) =>
    {
        // SETWAIT 会改 logo.WaitMode;先同步到 turtle,再触发一次性延时。
        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);

    logo.Execute(@"
        REPEAT 4 [
            FD 100
            RT 90
        ]
    ");

    turtle.Save("output.png");
}

Turtle 类公开了以下 API:

  • SetCanvasSize(int width, int height) — 设置画布大小
  • ShowTurtle() / HideTurtle() — 切换海龟可见性
  • Save(string fileName) — 将渲染的图像保存为文件
  • GetLineSegments() — 获取绘制的线段列表,用于自定义渲染
  • GetTurtleState() — 获取当前海龟的位置、角度和可见性
  • DelayMilliseconds — 控制绘图步骤之间的延迟。该延时仅作用于画线操作(PENDOWN / PENERASE)。非画线移动(如抬笔状态下的 SetX / SetY / SetXY / MoveForward)均为瞬移,不会因延时阻塞程序。

延时行为: - 0 毫秒:瞬间出图,无任何 sleep 开销。 - 119 毫秒:将多条线段合并为一次 sleep,以摊薄操作系统调度开销。例如,1 毫秒延时下画 20 条线段后统一睡眠 20 毫秒;10 毫秒延时下画 2 条线段后统一睡眠 20 毫秒。 - 20 毫秒及以上:每条线段独立等待,延时与设置值完全一致。

在 Windows 10(1803+)及以上,.NET 10.0/.NET Framework 4.8 环境下,延时通过 QuickTickLib 的 IO 完成端口实现高精度定时;在旧版 Windows 或不支持的平台上,自动回退到 Thread.Sleep

主要特性

AOTLogoSharp 提供以下命令和特性:

  • 基本画笔命令
  • PENDOWN/PD
  • PENUP/PU
  • SETPENCOLOR/SETPC/PC
  • SETPENSIZE
  • PENERASE/PE
  • PENNORMAL/PN
  • 基本绘图命令
  • LEFT/LT(逆时针)
  • RIGHT/RT(顺时针)
  • FORWARD/FD
  • BACKWARD/BK/BACK
  • DRAW/CLS/CLEARSCR/CLEARSCREEN/CS
  • 海龟控制命令
  • HOME
  • SHOWTURTLE/ST
  • HIDETURTLE/HT
  • SETX/SETY/SETXY
  • SETH/SETHEADING(默认 Logo 语义:0° = 正北,顺时针;设置 logo.HeadingMode = "standard" 可恢复数学标准:0° = 正东,逆时针)
  • 流程控制命令
  • REPEAT 和 RepCount
  • IF / IFELSE
  • WHILE
  • DELAY
  • WAIT
  • PRINT(字符串字面量、数值或列表字面量,会触发 Print 事件)
  • STOP(立即退出当前最内层过程)
  • OUTPUT/OP/RETURN
  • 语言特性
  • 变量(MAKE 命令)
  • 表达式
    • 算术运算:+-*/^
    • 比较运算:==<><><=>=
    • 逻辑运算:AND、OR、NOT
  • 过程(Procedures)
  • 函数调用
    • SQRT
    • RANDOM
    • SIN / COS / TAN (角度)
    • ASIN / ACOS / ATAN (角度),别名:ARCSIN / ARCCOS / ARCTAN
    • ABS
    • POWER
    • EXP
    • LOG (自然对数) / LOG10,别名:LN
  • 行内注释
  • 内置数学常量
    • PI
    • E

局限性

  • 代码编辑不支持多行格式
  • 除上述列出的命令外,其他 Logo 命令暂不支持。未来会逐步添加
  • 函数调用需要用大括号包裹,例如 {SQRT 2} 或 {RANDOM 100}。主要是因为函数调用本身是表达式,而其参数列表也是表达式,那么,如果不用括号这样的界定符括起来,语法上就会出现二义性。比如:hello world 2,你可以说hello是调用函数的函数名,它有两个参数:world和2,也可以说hello是一个没有参数的函数调用,而world是另一个函数,它的参数为2
  • PRINT "text 读取字面量直到下一个空白或分隔符(括号 / 方括号)为止;要在输出文本中包含空格,请改用列表字面量:PRINT [Hello World]
  • 引号标识符("name)与变量 / 关键字的匹配都是大小写不敏感的;但 PRINT "text 字面量内部的内容会按用户输入的原始大小写原样输出。

许可证

MIT