浏览代码

Version 1.0.3. Multiple modules compilation. Major bug fix. Documentation updated.

Artur Efimov 6 年之前
父节点
当前提交
5d2885688b

+ 363 - 0
Programs/GEVM.Mod

@@ -0,0 +1,363 @@
+(* Визуальный эмулятор ГЭВМ - Гипотетической электронно-вычислительной машины.
+ГЭВМ состоит из оперативной памяти на 256 байтов и регистра "программный счётчик".
+Перемещайтесь по ячейкам памяти с помощью клавиш со стрелками, вводите значения
+ячеек памяти при помощи цифр от 0 до 9 и букв от A до F.
+Зелёным цветом изображаются шестнадцатеричные числа, красным - десятичные.
+Клавиши:
+Табуляция - переставить курсор на следующую ячейку памяти и обнулить её.
+P - переключить курсор на программный счётчик (или обратно).
+R - выполнить инструкцию (процессор прорабатывает один такт).
+X - сменить основание системы счисления в данной ячейке с 10 на 16 (или обратно).
+Y - сменить основание системы счисления для вывода адресов ячеек.
+[ - привести основание системы счисления во всех ячейках к основанию адресов.
+] - перезагрузить ГЭВМ.
+Escape - закрыть эмулятор.
+
+Инструкции ГЭВМ:
+E4 - сложить два числа, адреса которых даны в первых двух параметрах,
+     и записать результат в ячейку по адресу, данному в третьем параметре.
+A1 - безусловный переход на адрес, данный в [единственном] параметре.
+B4 - уменьшить значение ячейки, адрес которой дан в первом параметре, на единицу.
+A2 - если значение по адресу, данному в первом параметре, равно нулю, перейти на
+     адрес, данному во втором параметре.
+00 - останов.
+
+6 апреля 2019 г. Рига. *)
+
+MODULE GEVM;
+IMPORT Files, G := Graph, Out;
+CONST 
+  w = 8; (* Ячеек ОЗУ в строке *)
+  h = 5; (* Ячеек ОЗУ в столбце *)
+  cw = 60; (* Ширина ячейки в пикселях *)
+  ch = 24; (* Высота ячейки в пикселях *)
+  intX = 13; (* Интервал между ячейками по горизонтали в пикселях *)
+  intY = 32; (* Интервал между ячейками по вертикали в пикселях *)
+
+  (* Клавиши *)
+  tab    = CHR(9);
+  backsp = CHR(8);
+  enter  = CHR(13);
+  esc    = CHR(27);
+  right  = CHR(79);
+  left   = CHR(80);
+  down   = CHR(81);
+  up     = CHR(82);
+
+VAR F: Files.File;
+  r: Files.Rider;
+  s: G.Bitmap;
+  font: G.Font;
+
+  mem: ARRAY 256 OF CHAR; (* ОЗУ *)
+  base10: ARRAY 256 OF BOOLEAN; (* Основание системы счисления = 10 *)
+  mark: ARRAY 256 OF BOOLEAN; (* Пометка изменений *)
+  pc: CHAR; (* Программный счётчик *)
+  off: BOOLEAN; (* Выключена ли ГЭВМ *)
+
+  first: INTEGER; (* Номер первой видимой ячейки ОЗУ *)
+  cur: INTEGER; (* Номер выделенной ячейки ОЗУ *)
+  curPC: BOOLEAN; (* Находится ли курсор на программном счётчике *)
+  addr10: BOOLEAN; (* Показываются ли адреса в десятичном виде *)
+
+  cx: INTEGER; (* Отступ слева в пикселях *)
+  cy: INTEGER; (* Отступ сверху в пикселях *)
+
+  (* Цвета *)
+  black: INTEGER;
+  white: INTEGER;
+  grey: INTEGER;
+  green, dkgreen, ltgreen: INTEGER;
+  red, dkred, ltred: INTEGER;
+  yellow: INTEGER;
+
+PROCEDURE ResetMem;
+VAR i: INTEGER;
+BEGIN
+  FOR i := 0 TO LEN(mem) - 1 DO
+    mem[i] := 0X;
+    base10[i] := TRUE
+  END;
+  pc := 0X; first := 0; cur := 0;
+  curPC := FALSE; addr10 := TRUE; off := TRUE
+END ResetMem;
+
+PROCEDURE ResetBase;
+VAR i: INTEGER;
+BEGIN
+  FOR i := 0 TO LEN(mem) - 1 DO base10[i] := addr10 END
+END ResetBase;
+
+PROCEDURE Init(): BOOLEAN;
+VAR i: INTEGER;
+  ok: BOOLEAN;
+BEGIN
+  ok := TRUE;
+  font := G.LoadFont("data/images/font.bmp", 8, 16);
+  IF font = NIL THEN
+    Out.String("Could not load font."); Out.Ln;
+    ok := FALSE
+  END;
+
+  IF ok THEN
+    cx := (s.w - (cw + intX) * w - intX) DIV 2;
+    cy := (s.h - (ch + intY) * h) DIV 2;
+
+    black   := G.MakeCol(  0,   0,   0);
+    white   := G.MakeCol(255, 255, 255);
+    grey    := G.MakeCol(120, 120, 120);
+    red     := G.MakeCol(230,   0,   0);
+    dkred   := G.MakeCol(100,   0,   0);
+    ltred   := G.MakeCol(255,  90,  90);
+    green   := G.MakeCol(  0, 230,   0);
+    dkgreen := G.MakeCol(  0, 100,   0);
+    ltgreen := G.MakeCol( 90, 255,  90);
+    yellow  := G.MakeCol(255, 255,  90);
+
+    ResetMem;
+    ResetBase
+  END;
+  RETURN ok
+END Init;
+
+PROCEDURE NumToStr(n: INTEGER; b10: BOOLEAN; VAR str: ARRAY OF CHAR);
+VAR i, b, x: INTEGER;
+  L, R: INTEGER;
+  c: CHAR;
+BEGIN
+  IF b10 THEN b := 10 ELSE b := 16 END;
+  i := 0;
+  REPEAT
+    x := n MOD b;
+    IF x < 10 THEN str[i] := CHR(ORD('0') + x)
+    ELSE str[i] := CHR(ORD('A') + x - 10)
+    END;
+    n := n DIV b;
+    INC(i)
+  UNTIL n = 0;
+  (* Возможно дописать 0 *)
+  IF i < 2 THEN str[i] := '0'; INC(i) END;
+  str[i] := 0X;
+  (* Перевенуть задом наперёд *)
+  L := 0; R := i - 1;
+  WHILE L < R DO c := str[L]; str[L] := str[R]; str[R] := c; INC(L); DEC(R) END
+END NumToStr;
+
+(* v - значение в ячейке;
+   type = 0 или 1, это тип (цвет) кнопки *)
+PROCEDURE DrawReg(v: INTEGER; b10: BOOLEAN; x0, y0, x1, y1, type: INTEGER;
+    shine: BOOLEAN);
+VAR str: ARRAY 16 OF CHAR;
+  c1, c2, c3: INTEGER;
+BEGIN
+  IF type = 0 THEN c1 := green; c2 := dkgreen; c3 := ltgreen
+  ELSE c1 := red; c2 := dkred; c3 := ltred
+  END;
+  G.RectFill(s, x0, y0, x1, y1, c3);
+  G.HLine(s, x0, y0, x1, c1);
+  G.VLine(s, x0, y0, y1, c1);
+  G.HLine(s, x0, y1, x1, c2);
+  G.VLine(s, x1, y0, y1, c2);
+  (* Внутри *)
+  NumToStr(v, b10, str);
+  G.DrawString(s, font, x0 + (cw - font.charW * 2) DIV 2,
+    y0 + (ch - font.charH) DIV 2, str, black);
+  (* Курсор *)
+  IF shine THEN
+    G.Rect(s, x0 - 1, y0 - 1, x1 + 1, y1 + 1, yellow);
+    G.Rect(s, x0 - 3, y0 - 3, x1 + 3, y1 + 3, yellow)
+  END
+END DrawReg;
+
+(* n - индекс ячейки;
+   type = 0 или 1, это тип (цвет) кнопки *)
+PROCEDURE DrawCell(n, x0, y0, x1, y1, type: INTEGER);
+VAR str: ARRAY 16 OF CHAR;
+  x, y, c: INTEGER;
+BEGIN
+  (* Подсветка изменений *)
+  IF n = ORD(pc) THEN
+    G.RectFill(s, x0 - 3, y0 - 3, x1 + 3, y1 + font.charH + 3, grey);
+    G.Rect(s, x0 - 3, y0 - 3, x1 + 3, y1 + font.charH + 3, white);
+    G.Rect(s, x0 - 4, y0 - 4, x1 + 4, y1 + font.charH + 4, ltred);
+    G.Rect(s, x0 - 5, y0 - 5, x1 + 5, y1 + font.charH + 5, red)
+  END;
+  
+  DrawReg(ORD(mem[n]), base10[n], x0, y0, x1, y1, type, (cur = n) & ~curPC);
+  (* Подпись внизу *)
+  NumToStr(n, addr10, str);
+  IF n = ORD(pc) THEN c := yellow
+  ELSIF addr10 THEN c := ltred
+  ELSE c := ltgreen
+  END;
+  x := x0 + (cw - font.charW * 2) DIV 2; y := y1 + 3;
+  G.DrawString(s, font, x, y, str, c);
+  IF mark[n] THEN
+    G.HLine(s, x - 1, y + font.charH - 2, x + font.charW * 2 - 1, yellow)
+  END
+END DrawCell;
+
+PROCEDURE DrawMem;
+VAR i, type: INTEGER;
+  last: INTEGER;
+  x, y: INTEGER;
+BEGIN
+  last := first + w * h - 1;
+  IF last >= LEN(mem) THEN last := LEN(mem) - 1 END;
+  x := cx; y := cy;
+  FOR i := first TO last DO
+    IF base10[i] THEN type := 1 ELSE type := 0 END;
+    DrawCell(i, x, y, x + cw - 1, y + ch - 1, type);
+    IF (i + 1) MOD w # 0 THEN INC(x, cw + intX)
+    ELSE INC(y, ch + intY); x := cx
+    END
+  END
+END DrawMem;
+
+(* Рисует регистр "программный счётчик" *)
+PROCEDURE DrawPC;
+VAR x, y, type, c: INTEGER;
+  str: ARRAY 8 OF CHAR;
+BEGIN
+  IF addr10 THEN type := 1; c := ltred ELSE type := 0; c := ltgreen END;
+  x := cx + (w - 1) * (cw + intX);
+  y := cy - ch - 20;
+  str := "Pr. sc."; str[0] := 8FX; str[1] := 0E0X;
+  str[4] := 0E1X; str[5] := 0E7X;
+  G.DrawString(s, font, x - 8 * font.charW,
+    y + (ch - font.charH) DIV 2, str, c);
+  DrawReg(ORD(pc), addr10, x, y, x + cw - 1, y + ch - 1, type, curPC)
+END DrawPC;
+
+PROCEDURE Move(c: CHAR);
+BEGIN
+  IF ~curPC THEN
+    IF c = up THEN DEC(cur, w)
+    ELSIF c = left THEN DEC(cur)
+    ELSIF c = right THEN INC(cur)
+    ELSE (* c = down *) INC(cur, w)
+    END;
+    IF cur < 0 THEN cur := 0
+    ELSIF cur >= LEN(mem) THEN cur := LEN(mem) - 1
+    END;
+    IF cur < first THEN DEC(first, w)
+    ELSIF cur >= first + w * h THEN INC(first, w)
+    END
+  END
+END Move;
+
+PROCEDURE Backspace;
+VAR b: INTEGER;
+BEGIN
+  IF curPC THEN
+    IF addr10 THEN b := 10 ELSE b := 16 END;
+    pc := CHR(ORD(pc) DIV b)
+  ELSE
+    IF base10[cur] THEN b := 10 ELSE b := 16 END;
+    mem[cur] := CHR(ORD(mem[cur]) DIV b)
+  END
+END Backspace;
+
+PROCEDURE Input(x: INTEGER);
+VAR v, v0, b: INTEGER;
+  b10: BOOLEAN;
+BEGIN
+  IF curPC THEN
+    b10 := addr10;
+    v0 := ORD(pc)
+  ELSE
+    b10 := base10[cur];
+    v0 := ORD(mem[cur])
+  END;
+  IF ~b10 OR (x < 10) THEN
+    IF b10 THEN b := 10 ELSE b := 16 END;
+    v := v0 * b + x;
+    IF v > 255 THEN v := v0 MOD b * b + x END;
+    IF curPC THEN pc := CHR(v) ELSE mem[cur] := CHR(v) END
+  END
+END Input;
+
+PROCEDURE Mark(n: INTEGER);
+BEGIN
+  mark[n] := TRUE
+END Mark;
+
+PROCEDURE ClearMarks;
+VAR i: INTEGER;
+BEGIN
+  FOR i := 0 TO LEN(mark) - 1 DO mark[i] := FALSE END
+END ClearMarks;
+
+PROCEDURE GoTick;
+VAR instr, p0, p1, p2, p3, p4, v1, v2, v3: INTEGER;
+BEGIN
+  ClearMarks;
+  off := FALSE;
+  p0 := ORD(pc); instr := ORD(mem[p0]);
+  p1 := (p0 + 1) MOD LEN(mem); v1 := ORD(mem[p1]);
+  p2 := (p0 + 2) MOD LEN(mem); v2 := ORD(mem[p2]);
+  p3 := (p0 + 3) MOD LEN(mem); v3 := ORD(mem[p3]);
+  p4 := (p0 + 4) MOD LEN(mem);
+  IF instr = 0E4H THEN
+    mem[v3] := CHR((ORD(mem[v1]) + ORD(mem[v2])) MOD 256);
+    Mark(v3);
+    pc := CHR(p4)
+  ELSIF instr = 0A1H THEN pc := mem[p1]
+  ELSIF instr = 0B4H THEN
+    mem[v1] := CHR((ORD(mem[v1]) - 1) MOD 256);
+    Mark(v1);
+    pc := CHR(p2)
+  ELSIF instr = 0A2H THEN
+    IF mem[v1] = 0X THEN pc := mem[p2]
+    ELSE pc := CHR(p3)
+    END
+  ELSIF instr = 0 THEN off := TRUE; pc := 0X
+  END
+END GoTick;
+
+PROCEDURE DrawMsg;
+VAR z: ARRAY 5 OF CHAR;
+BEGIN
+  IF off THEN
+    z[0] := 82X; z[1] := 9BX; z[2] := 08AX; z[3] := 08BX; z[4] := 0X;
+    G.DrawString(s, font, cx, cy - font.charH - intY, z, yellow)
+  END
+END DrawMsg;
+
+PROCEDURE Run;
+VAR done: BOOLEAN;
+  c: CHAR;
+BEGIN
+  done := FALSE;
+  REPEAT
+    G.ClearScreen;
+    DrawMem;
+    DrawPC;
+    DrawMsg;
+    G.Flip;
+    c := G.ReadKey();
+    IF (right <= c) & (c <= up) THEN Move(c)
+    ELSIF c = tab THEN Move(right); mem[cur] := 0X
+    ELSIF c = backsp THEN Backspace
+    ELSIF ("0" <= c) & (c <= "9") THEN Input(ORD(c) - ORD("0"))
+    ELSIF ("a" <= c) & (c <= "f") THEN Input(ORD(c) - ORD("a") + 10)
+    ELSIF c = "x" THEN base10[cur] := ~base10[cur]
+    ELSIF c = "y" THEN addr10 := ~addr10
+    ELSIF c = "p" THEN curPC := ~curPC
+    ELSIF c = "r" THEN GoTick
+    ELSIF c = "[" THEN ResetBase
+    ELSIF c = "]" THEN ResetMem
+    ELSIF c = esc THEN done := TRUE
+    END
+  UNTIL done
+END Run;
+
+BEGIN
+  G.Settings(640, 400, {G.fullscreen, G.sharpPixels, G.spread});
+  s := G.Init();
+  IF (s # NIL) & Init() THEN
+    Run
+  ELSE Out.String("Init failed."); Out.Ln
+  END;
+  G.Close
+END GEVM.

+ 5 - 3
README.md

@@ -1,8 +1,8 @@
 ![Free Oberon screenshot](http://freeoberon.su/images/screenshot.png)
 
 # Free Oberon
-* Version 1.0.1
-* Riga, May 17, 2018
+* Version 1.0.3
+* Riga, June 18, 2019
 * Website: [freeoberon.su](http://freeoberon.su/en), [на русском](http://freeoberon.su)
 
 # Installation
@@ -44,9 +44,11 @@ Download the setup porgram in EXE format from [freeoberon.su](http://freeoberon.
 
 Alternatively, you can download a version of Free Oberon in a ZIP-archive (from [freeoberon.su](http://freeoberon.su)), extract it to any place on the disk and (optionally) create a desktop shortcut.
 
-Note. If you want to recompile Free Oberon under Windows from the source code yourself, refer to Appendix A of the [Free Oberon documentation on freeoberon.su](http://freeoberon.su/files/FreeOberon_v0.1.0_en.pdf).
+Note. If you want to recompile Free Oberon under Windows from the source code yourself, refer to Appendix A of the [Free Oberon documentation on freeoberon.su](http://freeoberon.su/files/FreeOberon_v1.0.3_en.pdf), [на русском](FreeOberon_v1.0.3.pdf).
 
 # Usage
 
 Run Free Oberon and type in an Oberon program (or open an example program like `Book.Mod`) and press `F9` to compile and run the program.
 The module source code files are saved in subdirectory `Programs` and the compiled executable files are saved in `bin`. `data/bin/compile.sh` and `data\bin\compile.bat` are used to compile a program on GNU/Linux and Windows accordingly and can be edited if required.
+Since version 1.0.3 it is also possible to compile and run programs that consist of several modules. Put all modules into `Programs` directory, open the main module and press `F9`. If there is an error in one of the modules of your program, the corresponding file will open up and the error will be highlighted. To recompile, focus the main module again and press `F9`.
+If module Graph is used, then SDL2 library will be automatically linked to your program.

+ 3 - 12
data/bin/compile.bat

@@ -1,20 +1,11 @@
 @ECHO OFF
-REM This script is run by Free Oberon on Windows.
-REM Current directory of the script will be where
-REM FreeOberon.exe is located.
-REM This particular script is for graphical programs.
+REM This script is run by Free Oberon on Windows. Current directory of the
+REM script will be where FreeOberon.exe is located.
 CD bin >nul 2>&1
-@DEL /s %~n1.exe >nul 2>&1
 SET CURDIR=%~dp0
 SET PATH=%CURDIR%voc\bin;%CURDIR%mingw32\bin;%PATH%
 ECHO ON
-@CALL voc -OC -cfFm ..\Programs\%1
+@CALL voc -OC -cfF %2 ..\Programs\%1
 @SET MYEXITCODE=%ERRORLEVEL%
 @ECHO OFF
-IF "%MYEXITCODE%"=="0" GOTO OK1
-GOTO END1
-:OK1
-gcc -fPIC -g -I "%CURDIR%voc/C/include" -o %~n1.exe %~n1.o "%CURDIR%voc/lib/Graph.o" "%CURDIR%voc/lib/SDL2.o" -L"%CURDIR%voc/lib" -lvoc-OC -w -Wl,-subsystem,windows -lmingw32 -lSDL2main -lSDL2 -lSDL2_image
-SET MYEXITCODE=%ERRORLEVEL%
-:END1
 EXIT /b %MYEXITCODE%

+ 3 - 16
data/bin/compile.sh

@@ -1,23 +1,10 @@
 #!/bin/bash
-# This script is run by FreeOberon on Linux.
-# Current directory of the script will be where
-# FreeOberon executable is located.
-# This particular script is for graphical programs.
+# This script is run by Free Oberon on Linux. Current directory of the
+# script will be where FreeOberon executable is located.
 cd bin
 VOCDIR=../data/bin/voc
 VOC=$VOCDIR/bin/voc
-CC=gcc
-$VOC -OC -cfFm ../Programs/$1
+$VOC -OC -cfF $2 ../Programs/$1
 retcode=$?
-if [ "$retcode" -eq "0" ]; then
-  THENAME="${1%.*}"
-  ONAME="${THENAME##*/}"
-  SDL2Opts=`sdl2-config --cflags --libs`
-  $CC -fPIC -g -I $VOCDIR/C/include \
-    -o $ONAME $ONAME.o \
-    $VOCDIR/lib/Graph.o $VOCDIR/lib/SDL2.o \
-    $VOCDIR/lib/libvoc-OC.a \
-    $SDL2Opts -lSDL2_image
-fi
 cd ..
 exit $retcode

+ 0 - 14
data/bin/compile_no_exe.bat

@@ -1,14 +0,0 @@
-@ECHO OFF
-REM This script is run by Free Oberon on Windows.
-REM Current directory of the script will be where
-REM FreeOberon.exe is located.
-REM This particular script is for compilation with no executable.
-CD bin >nul 2>&1
-@DEL /s %~n1.exe >nul 2>&1
-SET CURDIR=%~dp0
-SET PATH=%CURDIR%voc\bin;%CURDIR%mingw32\bin;%PATH%
-ECHO ON
-@CALL voc -OC -cfFm ..\Programs\%1
-@SET MYEXITCODE=%ERRORLEVEL%
-@ECHO OFF
-EXIT /b %MYEXITCODE%

+ 0 - 12
data/bin/compile_no_exe.sh

@@ -1,12 +0,0 @@
-#!/bin/bash
-# This script is run by FreeOberon on Linux.
-# Current directory of the script will be where
-# FreeOberon executable is located.
-# This particular script is for compilation with no executable.
-cd bin
-VOCDIR=../data/bin/voc
-VOC=$VOCDIR/bin/voc
-$VOC -OC -cfFm ../Programs/$1
-retcode=$?
-cd ..
-exit $retcode

+ 0 - 20
data/bin/compile_no_graph.bat

@@ -1,20 +0,0 @@
-@ECHO OFF
-REM This script is run by Free Oberon on Windows.
-REM Current directory of the script will be where
-REM FreeOberon.exe is located.
-REM This particular script is for console programs.
-CD bin >nul 2>&1
-@DEL /s %~n1.exe >nul 2>&1
-SET CURDIR=%~dp0
-SET PATH=%CURDIR%voc\bin;%CURDIR%mingw32\bin;%PATH%
-ECHO ON
-@CALL voc -OC -cfFm ..\Programs\%1
-@SET MYEXITCODE=%ERRORLEVEL%
-@ECHO OFF
-IF "%MYEXITCODE%"=="0" GOTO OK1
-GOTO END1
-:OK1
-gcc -fPIC -g -I "%CURDIR%voc/C/include" -o %~n1.exe %~n1.o -L"%CURDIR%voc/lib" -lvoc-OC -lmingw32
-SET MYEXITCODE=%ERRORLEVEL%
-:END1
-EXIT /b %MYEXITCODE%

+ 0 - 19
data/bin/compile_no_graph.sh

@@ -1,19 +0,0 @@
-#!/bin/bash
-# This script is run by FreeOberon on Linux.
-# Current directory of the script will be where
-# FreeOberon executable is located.
-# This particular script is for console programs.
-cd bin
-VOCDIR=../data/bin/voc
-VOC=$VOCDIR/bin/voc
-CC=gcc
-$VOC -OC -cfFm ../Programs/$1
-retcode=$?
-if [ "$retcode" -eq "0" ]; then
-  THENAME="${1%.*}"
-  $CC -fPIC -g -I $VOCDIR/C/include \
-    -o $THENAME $THENAME.o \
-    $VOCDIR/lib/libvoc-OC.a
-fi
-cd ..
-exit $retcode

+ 12 - 0
data/bin/link_console.bat

@@ -0,0 +1,12 @@
+@ECHO OFF
+REM This script is run by Free Oberon on Windows. Current directory of the
+REM script will be where FreeOberon.exe is located. This
+REM particular script is for console programs.
+CD bin >nul 2>&1
+@DEL /s %~n1.exe >nul 2>&1
+SET CURDIR=%~dp0
+SET PATH=%CURDIR%voc\bin;%CURDIR%mingw32\bin;%PATH%
+ECHO ON
+gcc -fPIC -g -I "%CURDIR%voc/C/include" -o %~n1.exe %~n1.o -L"%CURDIR%voc/lib" -lvoc-OC -lmingw32
+SET MYEXITCODE=%ERRORLEVEL%
+EXIT /b %MYEXITCODE%

+ 18 - 0
data/bin/link_console.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+# This script is run by Free Oberon on Linux. Current directory of the
+# script will be where FreeOberon executable is located. This
+# particular script is for console programs.
+cd bin
+VOCDIR=../data/bin/voc
+CC=gcc
+THENAME="${1%.*}"
+ONAME="${THENAME##*/}"
+SDL2Opts=`sdl2-config --cflags --libs`
+shift
+$CC -fPIC -g -I $VOCDIR/C/include \
+  -o $ONAME $ONAME.o \
+  $@ \
+  $VOCDIR/lib/libvoc-OC.a
+retcode=$?
+cd ..
+exit $retcode

+ 12 - 0
data/bin/link_graph.bat

@@ -0,0 +1,12 @@
+@ECHO OFF
+REM This script is run by Free Oberon on Windows. Current directory of the
+REM script will be where FreeOberon.exe is located. This
+REM particular script is for graphical programs.
+CD bin >nul 2>&1
+@DEL /s %~n1.exe >nul 2>&1
+SET CURDIR=%~dp0
+SET PATH=%CURDIR%voc\bin;%CURDIR%mingw32\bin;%PATH%
+ECHO ON
+gcc -fPIC -g -I "%CURDIR%voc/C/include" -o %~n1.exe %~n1.o "%CURDIR%voc/lib/Graph.o" "%CURDIR%voc/lib/SDL2.o" -L"%CURDIR%voc/lib" -lvoc-OC -w -Wl,-subsystem,windows -lmingw32 -lSDL2main -lSDL2 -lSDL2_image
+SET MYEXITCODE=%ERRORLEVEL%
+EXIT /b %MYEXITCODE%

+ 20 - 0
data/bin/link_graph.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+# This script is run by Free Oberon on Linux. Current directory of the
+# script will be where FreeOberon executable is located. This
+# particular script is for graphical programs.
+cd bin
+VOCDIR=../data/bin/voc
+CC=gcc
+THENAME="${1%.*}"
+ONAME="${THENAME##*/}"
+SDL2Opts=`sdl2-config --cflags --libs`
+shift
+$CC -fPIC -g -I $VOCDIR/C/include \
+  -o $ONAME $ONAME.o \
+  $@ \
+  $VOCDIR/lib/Graph.o $VOCDIR/lib/SDL2.o \
+  $VOCDIR/lib/libvoc-OC.a \
+  $SDL2Opts -lSDL2_image
+retcode=$?
+cd ..
+exit $retcode

+ 186 - 86
src/FreeOberon.Mod

@@ -50,7 +50,7 @@ CONST
 TYPE
   StrList = POINTER TO StrListDesc;
   StrListDesc = RECORD
-    s: ARRAY 120 OF CHAR;
+    s: ARRAY 256 OF CHAR;
     next: StrList
   END;
 
@@ -62,6 +62,7 @@ VAR
   tempWindowed: BOOLEAN; (* True if editor is in windowed mode while program is running *)
   needWindowed: BOOLEAN;
   blockToggle: BOOLEAN; (* If true, ALT+ENTER will not toggle fullscreen *)
+  sysModules: StrList;
 
   app: OV.App;
 
@@ -163,7 +164,54 @@ BEGIN
   END
 END StringsFindNext;
 
-PROCEDURE ParseErrors(VAR s: ARRAY OF CHAR);
+PROCEDURE FileNew(c: OV.Control);
+VAR e: Editor.Editor;
+  p, br: OV.Control;
+  count: INTEGER;
+BEGIN e := Editor.NewEditor(); 
+  p := app.windows; br := p; count := 0;
+  WHILE p # NIL DO INC(count);
+    IF p.next = br THEN p := NIL ELSE p := p.next END
+  END;
+  IF app.windows # NIL THEN
+    e.x := app.windows.x + 1; e.y := app.windows.y + 1;
+    e.w := app.windows.w - 1; e.h := app.windows.h - 1
+  END;
+  e.caption := 'NONAME??.Mod';
+  e.caption[6] := CHR(ORD('0') + count DIV 10 MOD 10);
+  e.caption[7] := CHR(ORD('0') + count MOD 10);
+  OV.AddWindow(app, e)
+END FileNew;
+
+PROCEDURE DoOpenFile(filename: ARRAY OF CHAR);
+VAR e: Editor.Editor; newWin: BOOLEAN;
+BEGIN
+  IF (app.windows # NIL) & (app.windows IS Editor.Editor) THEN
+    e := app.windows(Editor.Editor)
+  ELSE e := NIL
+  END;
+  newWin := (e = NIL) OR ~Editor.IsEmpty(e);
+  IF newWin THEN e := Editor.NewEditor() END;
+  IF e.text.LoadFromFile(filename) THEN
+    e.caption := filename; e.filename := filename;
+    IF newWin THEN OV.AddWindow(app, e) END
+  END
+END DoOpenFile;
+
+PROCEDURE FocusOrOpenFile(filename: ARRAY OF CHAR);
+VAR e, f: Editor.Editor;
+BEGIN
+  e := app.windows(Editor.Editor); f := e;
+  WHILE (e # NIL) & (e.filename # filename) DO
+    IF e.next = f THEN e := NIL ELSE e := e.next(Editor.Editor) END
+  END;
+  IF e = NIL THEN DoOpenFile(filename)
+  ELSE app.windows := e; OV.SetFocus(app, e)
+  END;
+  OV.DrawApp(app)
+END FocusOrOpenFile;
+
+PROCEDURE ParseErrors(VAR s: ARRAY OF CHAR; filename: ARRAY OF CHAR);
 VAR i, j, pos, st, len, skip: INTEGER; found: BOOLEAN;
 BEGIN
   StringsFindNext(' pos ', s, 0, found, i);
@@ -188,6 +236,7 @@ BEGIN
         s[j] := '.'; INC(j); s[j] := 0X;
         (* Capitalize first letter (0th is a space). *)
         IF (s[1] >= 'a') & (s[1] <= 'z') THEN s[1] := CAP(s[1]) END;
+        FocusOrOpenFile(filename);
         app.windows(Editor.Editor).text.MoveToPos(pos);
         Editor.PrintText(app.windows(Editor.Editor));
         T.ResetCursorBlink (* !FIXME *)
@@ -208,6 +257,7 @@ PROCEDURE PollProgram;
 VAR len, i: INTEGER;
     err: INTEGER;
     s, sN: ARRAY 120 OF CHAR;
+
   PROCEDURE WriteProgBuf;
   VAR ch: CHAR; i: INTEGER;
   BEGIN
@@ -302,12 +352,13 @@ BEGIN
         IF blockToggle THEN blockToggle := FALSE
         ELSE T.ToggleFullscreen; blockToggle := TRUE
         END
-      ELSE T.Ln; WriteToProcess(inputBuf, inputBufLen);
+      ELSE T.Ln;
+        WriteToProcess(inputBuf, inputBufLen);
         inputBufLen := 0; buf[0] := 0AX;
         Term.WriteToProcess(buf, 1)
       END
     | G.kBackspace:
-      IF (inputBufLen > 0) THEN
+      IF inputBufLen > 0 THEN
         DEC(inputBufLen); T.Backspace
       END
     | G.kPause:
@@ -348,51 +399,134 @@ BEGIN quit := FALSE;
   UNTIL quit
 END RunTerminal;
 
-PROCEDURE Compile(filename: ARRAY OF CHAR; graph: BOOLEAN): BOOLEAN;
+PROCEDURE IsSysModule(name: ARRAY OF CHAR): BOOLEAN;
+VAR p: StrList;
+BEGIN p := sysModules;
+  WHILE (p # NIL) & (p.s # name) DO p := p.next END;
+  RETURN p # NIL
+END IsSysModule;
+
+PROCEDURE ModuleExists(s: ARRAY OF CHAR): BOOLEAN;
+VAR fname: ARRAY 120 OF CHAR;
+  F: Files.File;
+  exists: BOOLEAN;
+BEGIN
+  fname := 'Programs/'; Strings.Append(s, fname);
+  Strings.Append('.Mod', fname);
+  F := Files.Old(fname);
+  exists := F # NIL;
+  IF F # NIL THEN Files.Close(F) END;
+RETURN exists END ModuleExists;
+
+PROCEDURE RunCommand(filename: ARRAY OF CHAR;
+    link, graph, main: BOOLEAN; list: StrList): BOOLEAN;
 CONST bufLen = 20480;
 VAR buf: ARRAY bufLen OF CHAR;
+    p: StrList;
     len, err: INTEGER;
-    scriptPostfix: ARRAY 32 OF CHAR;
+    command: ARRAY 32 OF CHAR;
     cmd: ARRAY 1024 OF CHAR;
     s, sN: ARRAY 80 OF CHAR;
     success: BOOLEAN;
 BEGIN
-  IF ~graph THEN scriptPostfix := '_no_graph' ELSE scriptPostfix := '' END;
+  IF ~link THEN command := 'compile'
+  ELSIF graph THEN command := 'link_graph'
+  ELSE command := 'link_console'
+  END;
   IF Config.isWindows THEN
     IF Term.SearchPath('cmd.exe', cmd) # 0 THEN
       Strings.Insert('"', 0, cmd);
-      Strings.Append('" /C data\bin\compile', cmd);
-      Strings.Append(scriptPostfix, cmd);
+      Strings.Append('" /C data\bin\', cmd);
+      Strings.Append(command, cmd);
       Strings.Append('.bat ', cmd)
     ELSE T.PutString(0, T.charsY - 1, 'Could not find cmd.exe', 15, 4, 0)
     END
   ELSE (* Linux *)
-    COPY('data/bin/compile', cmd);
-      Strings.Append(scriptPostfix, cmd);
-      Strings.Append('.sh ', cmd)
+    COPY('data/bin/', cmd); Strings.Append(command, cmd);
+    Strings.Append('.sh ', cmd)
   END;
   Strings.Append(filename, cmd);
+  IF main THEN Strings.Append(' -m', cmd)
+  ELSIF link & (list # NIL) THEN
+    p := list;
+    WHILE p.next # NIL DO
+      IF ModuleExists(p.s) THEN
+        Strings.Append(' ', cmd); Strings.Append(p.s, cmd);
+        Strings.Append('.o', cmd)
+      END;
+      p := p.next
+    END
+  END;
   success := (Term.RunProcess(cmd, buf, bufLen, len, err) # 0) &
              (err = 0);
   IF ~success THEN
-    COPY(' Compilation returned ', s);
-    IntToStr(err, sN);
-    Strings.Append(sN, s);
+    s := ' Command returned ';
+    IntToStr(err, sN); Strings.Append(sN, s);
     Strings.Append(' exit status ', s);
     IF (len > 0) & (len < bufLen) THEN
       IF buf[len - 1] = 0AX THEN buf[len - 1] := 0X
       ELSE buf[len] := 0X
       END;
-      ParseErrors(buf)
-    ELSE COPY(' Compilation failed.', buf)
+      ParseErrors(buf, filename)
+    ELSIF link THEN buf := 'Linking failed.'
+    ELSE buf := 'Compilation failed.'
     END;
     IF buf[0] = 0X THEN ShowErrors(s)
     ELSE ShowErrors(buf)
     END
   END;
   RETURN success
+END RunCommand;
+
+PROCEDURE Compile(filename: ARRAY OF CHAR; main: BOOLEAN): BOOLEAN;
+BEGIN RETURN RunCommand(filename, FALSE, FALSE, main, NIL)
 END Compile;
 
+PROCEDURE Link(filename: ARRAY OF CHAR;
+    graph: BOOLEAN; list: StrList): BOOLEAN;
+BEGIN RETURN RunCommand(filename, TRUE, graph, FALSE, list)
+END Link;
+
+PROCEDURE ResetSysModules;
+  PROCEDURE Add(s: ARRAY OF CHAR);
+  VAR p: StrList;
+  BEGIN NEW(p); p.s := s; p.next := sysModules; sysModules := p
+  END Add;
+BEGIN sysModules := NIL;
+  Add('SYSTEM');  Add('Texts');    Add('Files');  Add('Strings');
+  Add('In');      Add('Out');      Add('Math');   Add('MathL');
+  Add('Modules'); Add('Platform'); Add('Oberon'); Add('Reals');
+  Add('VT100');   Add('Graph');    Add('SDL2');   Add('Term')
+END ResetSysModules;
+
+PROCEDURE CompileAll(modules: StrList): BOOLEAN;
+VAR s: ARRAY 256 OF CHAR;
+  p, last: StrList;
+  ok, graph: BOOLEAN;
+BEGIN
+  IF modules # NIL THEN
+    ok := TRUE; p := modules; graph := FALSE;
+    WHILE ok & (p.next # NIL) DO
+      IF ModuleExists(p.s) THEN
+        s := p.s; Strings.Append('.Mod', s);
+        IF ~Compile(s, FALSE) THEN ok := FALSE END
+      ELSIF IsSysModule(p.s) THEN
+        IF p.s = 'Graph' THEN graph := TRUE END
+      ELSE ok := FALSE
+      END;
+      p := p.next
+    END;
+    IF ok THEN
+      IF ModuleExists(p.s) THEN
+        s := p.s; Strings.Append('.Mod', s);
+        IF ~Compile(s, TRUE) THEN ok := FALSE END
+      END;
+      IF ~Link(p.s, graph, modules) THEN ok := FALSE END
+    END
+  ELSE ok := FALSE
+  END;
+RETURN ok END CompileAll;
+
 PROCEDURE RunProgram(prg: ARRAY OF CHAR);
 VAR cmd: ARRAY 128 OF CHAR;
     x: INTEGER;
@@ -417,39 +551,9 @@ BEGIN
   END
 END RunProgram;
 
-PROCEDURE FileNew(c: OV.Control);
-VAR e: Editor.Editor;
-  p, br: OV.Control;
-  count: INTEGER;
-BEGIN e := Editor.NewEditor(); 
-  p := app.windows; br := p; count := 0;
-  WHILE p # NIL DO INC(count);
-    IF p.next = br THEN p := NIL ELSE p := p.next END
-  END;
-  IF app.windows # NIL THEN
-    e.x := app.windows.x + 1; e.y := app.windows.y + 1;
-    e.w := app.windows.w - 1; e.h := app.windows.h - 1
-  END;
-  e.caption := 'NONAME??.Mod';
-  e.caption[6] := CHR(ORD('0') + count DIV 10 MOD 10);
-  e.caption[7] := CHR(ORD('0') + count MOD 10);
-  OV.AddWindow(app, e)
-END FileNew;
-
-PROCEDURE DoOpenFile(c: OV.Control; filename: ARRAY OF CHAR);
-VAR e: Editor.Editor; newWin: BOOLEAN;
-BEGIN
-  IF (c.app.windows # NIL) & (c.app.windows IS Editor.Editor) THEN
-    e := c.app.windows(Editor.Editor)
-  ELSE e := NIL
-  END;
-  newWin := (e = NIL) OR ~Editor.IsEmpty(e);
-  IF newWin THEN e := Editor.NewEditor() END;
-  IF e.text.LoadFromFile(filename) THEN
-    e.caption := filename; e.filename := filename;
-    IF newWin THEN OV.AddWindow(app, e) END
-  END
-END DoOpenFile;
+PROCEDURE OpenFileOkClick(c: OV.Control; filename: ARRAY OF CHAR);
+BEGIN DoOpenFile(filename)
+END OpenFileOkClick;
 
 PROCEDURE DoSaveFile(c: OV.Control; filename: ARRAY OF CHAR);
 VAR w: OV.Window; e: Editor.Editor;
@@ -467,7 +571,7 @@ PROCEDURE FileOpen(c: OV.Control);
 VAR w: Editor.FileDialog;
 BEGIN
   w := Editor.NewFileDialog(Editor.open);
-  w.onFileOk := DoOpenFile;
+  w.onFileOk := OpenFileOkClick;
   OV.AddWindow(app, w)
 END FileOpen;
 
@@ -548,28 +652,15 @@ BEGIN
   s[i] := 0X
 END GetSym;
 
-PROCEDURE ModuleExists(s: ARRAY OF CHAR): BOOLEAN;
-VAR fname: ARRAY 120 OF CHAR;
-  F: Files.File;
-  exists: BOOLEAN;
-BEGIN
-  fname := 'Programs/'; Strings.Append(s, fname);
-  Strings.Append('.Mod', fname);
-  F := Files.Old(fname);
-  exists := F # NIL;
-  IF F # NIL THEN Files.Close(F) END;
-  IF s = 'Graph' THEN exists := TRUE END; (*!FIXME Temporary exception*)
-RETURN exists END ModuleExists;
-
-PROCEDURE GetImportedModules(filename: ARRAY OF CHAR): StrList;
+PROCEDURE GetImportedModules(modname: ARRAY OF CHAR): StrList;
 VAR F: Files.File;
   R: Files.Rider;
   top, p: StrList;
   ch: CHAR;
-  mod, s: ARRAY 120 OF CHAR;
+  mod, s: ARRAY 256 OF CHAR;
   ok: BOOLEAN;
 BEGIN NEW(top); top.next := NIL; p := top;
-  s := 'Programs/'; Strings.Append(filename, s);
+  s := 'Programs/'; Strings.Append(modname, s); Strings.Append('.Mod', s);
   F := Files.Old(s);
   IF F # NIL THEN
     Files.Set(R, F, 0); Files.Read(R, ch); GetSym(R, ch, s);
@@ -582,7 +673,7 @@ BEGIN NEW(top); top.next := NIL; p := top;
           mod := s;
           GetSym(R, ch, s);
           IF s = ':=' THEN GetSym(R, ch, s); mod := s; GetSym(R, ch, s) END;
-          IF ModuleExists(mod) THEN
+          IF IsSysModule(mod) OR ModuleExists(mod) THEN
             NEW(p.next); p := p.next; p.next := NIL; p.s := mod
           END;
           IF s = ',' THEN GetSym(R, ch, s) ELSE ok := FALSE END
@@ -590,8 +681,7 @@ BEGIN NEW(top); top.next := NIL; p := top;
       END
     END
   END;
-  RETURN top.next
-END GetImportedModules;
+RETURN top.next END GetImportedModules;
 
 PROCEDURE DebugStrList(p: StrList);
 BEGIN
@@ -619,40 +709,49 @@ BEGIN
   END
 END AddUniqueToList;
 
-PROCEDURE GetAllImportedModules(filename: ARRAY OF CHAR): StrList;
-VAR list, list2, p: StrList;
-  s: ARRAY 120 OF CHAR;
-BEGIN
-  list := GetImportedModules(filename);
+PROCEDURE UsedModuleList(modname: ARRAY OF CHAR): StrList;
+VAR res, list, list2, p: StrList;
+BEGIN res := NIL;
+  list := GetImportedModules(modname);
   p := list;
   WHILE p # NIL DO
-    s := p.s;
-    Strings.Append(".Mod", s);
-    list2 := GetImportedModules(s);
-    AddUniqueToList(list2, list);
+    list2 := UsedModuleList(p.s);
+    AddUniqueToList(list2, res);
     p := p.next
   END;
-RETURN list END GetAllImportedModules;
+  NEW(p); p.s := modname; p.next := NIL; AddUniqueToList(p, res);
+RETURN res END UsedModuleList;
 
 PROCEDURE ImportsGraph(p: StrList): BOOLEAN;
 BEGIN 
   WHILE (p # NIL) & (p.s # 'Graph') DO p := p.next END;
 RETURN p # NIL END ImportsGraph;
 
+(* "Module.Mod" -> "Module" *)
+PROCEDURE GetModuleName(filename: ARRAY OF CHAR; VAR modname: ARRAY OF CHAR);
+VAR i: INTEGER;
+BEGIN i := 0;
+  WHILE (filename[i] # 0X) & (filename[i] # '.') DO
+    modname[i] := filename[i]; INC(i)
+  END;
+  modname[i] := 0X
+END GetModuleName;
+
 PROCEDURE OnBuild(c: OV.Control);
 VAR w: OV.Window; graph: BOOLEAN;
-  primaryFile: ARRAY 256 OF CHAR;
-  p: StrList;
+  primaryFile, modname: ARRAY 256 OF CHAR;
+  modules: StrList;
 BEGIN w := c.app.windows;
   IF (w # NIL) & (w IS Editor.Editor) THEN
     IF Editor.TextChanged(w(Editor.Editor)) THEN FileSave(c) END;
     IF w(Editor.Editor).filename[0] # 0X THEN
       COPY(w(Editor.Editor).filename, primaryFile);
-      p := GetAllImportedModules(primaryFile);
-      (*DebugStrList(p);*)
-      graph := ImportsGraph(p);
+      GetModuleName(primaryFile, modname);
+      modules := UsedModuleList(modname);
+      (*DebugStrList(modules);*)
+      graph := ImportsGraph(modules);
       needWindowed := graph;
-      IF Compile(w(Editor.Editor).filename, graph) THEN
+      IF CompileAll(modules) THEN
         tempWindowed := needWindowed & T.isFullscreen;
         IF tempWindowed THEN G.SwitchToWindowed END;
         RunProgram(w(Editor.Editor).filename)
@@ -825,6 +924,7 @@ BEGIN
     InitIDE;
     needWindowed := TRUE;
     blockToggle := FALSE;
+    ResetSysModules;
     success := TRUE
   ELSE Out.String('Terminal init failed.'); Out.Ln
   END;

+ 1 - 1
src/Makefile_linux

@@ -49,7 +49,7 @@ Graph.sym: Graph.Mod SDL2.sym
 SDL2.sym: SDL2.Mod
 	$(VOC) -OC -cesF SDL2.Mod
 
-.PHONY: clean cleanall install pack prepare
+.PHONY: clean cleanfo cleanall install pack prepare
 
 cleanfo:
 	rm -f *.c *.h *.o *.sym term/term.o .tmp..* \

+ 3 - 4
src/Terminal.Mod

@@ -292,10 +292,9 @@ PROCEDURE Backspace*;
 BEGIN
   needRedraw := TRUE;
   chars[cursorY, cursorX].updated := TRUE;
-  IF cursorX = 0 THEN
-    cursorX := charsX - 1;
-    DEC(cursorY)
-  ELSE DEC(cursorX) END;
+  IF cursorX # 0 THEN DEC(cursorX)
+  ELSIF cursorY # 0 THEN cursorX := charsX - 1; DEC(cursorY)
+  END;
   chars[cursorY, cursorX].ch := ' ';
   chars[cursorY, cursorX].updated := TRUE
 END Backspace;