<> <> <> <> <> <> <> DIRECTORY Atom, Basics, CD, CDProperties, CDRects, CDCells, CDDirectory, CDOps, CDPolygons, CDViewer, CMos, IntDefs, IntStorageDefs, IntTransDefs, IntUtilityDefs, IO, IODefs, OutputDefs, ParserTypeDefs, PolygonDefs, ReadCif, ReadCifUserCmd, Real, RealFns, Rope; CIFIntPhase2: CEDAR PROGRAM IMPORTS Atom, CD, CDProperties, CDRects, CDCells, CDDirectory, CDOps, CDPolygons, CDViewer, IntTransDefs, IntStorageDefs, IntUtilityDefs, IO, IODefs, PolygonDefs, ReadCif, ReadCifUserCmd, Real, RealFns, Rope EXPORTS IntDefs, OutputDefs, ReadCif = BEGIN OPEN IntUtilityDefs, IntStorageDefs; CIFMap: TYPE = RECORD [ size: NAT _ 0, map: SEQUENCE maxSize: NAT OF CDCIFRelation ]; CDCIFRelation: TYPE = RECORD[ cifName: ATOM, cdLevel: CD.Layer, compensation: INT _ 0 ]; cifMap: REF CIFMap; CIFDestRec: TYPE = RECORD [ -- from BrandyCIFter.mesa, wasn't in an interface... cifDest: Rope.ROPE, deltaRadius: INT -- nanometers, + means cif is bigger ]; technology: PUBLIC ATOM _ NIL; design: PUBLIC CD.Design _ NIL; cdLambda: CD.Number _ 0; cifLambda: CARDINAL _ 0; unknownLayers: LIST OF ATOM _ NIL; nanometersPerCifUnit: INT = 10; nextCellNumber: INT _ 0; numSides: CARDINAL _ 12; ErrorMsgType: TYPE = ReadCifUserCmd.ErrorMsgType; Map: PUBLIC PROCEDURE [layerName: Rope.ROPE] RETURNS [n: CARDINAL] = BEGIN cm: REF CIFMap; ccr: REF CDCIFRelation _ NIL; cifLayerAtom: ATOM = Atom.MakeAtom[layerName]; -- first time through, initialize data structures IF cifMap = NIL THEN BEGIN cifMap _ NEW[CIFMap[1]]; cifMap.size _ 1; IF CD.FetchTechnology[ReadCif.technology] = NIL THEN {IODefs.WriteLine["Error: Chipndale technology not loaded"]; Abort[]}; IF design=NIL THEN { design _ CDOps.CreateDesign[CD.FetchTechnology[ReadCif.technology]]; design.name _ ReadCif.designName; }; cdLambda _ design.technology.lambda; cifLambda _ ReadCif.cifUnitsPerLambda; IF cifLambda < 1 THEN {IODefs.WriteLine["Error: CIF units per lambda is too small"]; Abort[]}; END; --if layer is defined in map, return the mapped number FOR n IN [0..cifMap.size) DO IF cifMap[n].cifName = cifLayerAtom THEN RETURN; ENDLOOP; <<-- see if it's an unknown layer that's been seen before>> FOR u: LIST OF ATOM _ unknownLayers, u.rest WHILE u#NIL DO IF u.first = cifLayerAtom THEN RETURN[LAST[CARDINAL]]; ENDLOOP; <<>> <<-- layer never seen before, see if there is a corresponding Chipndale Level>> ccr _ FindCDLayer[layerName]; IF ccr = NIL THEN { <<-- no corresponding Chipndale Level, add to unknownLayers>> unknownLayers _ CONS[cifLayerAtom, unknownLayers]; IODefs.WriteLine[Rope.Cat["CIF layer ", layerName, " is not registered with ChipNDale, objects on this layer will be deleted"]]; RETURN[LAST[CARDINAL]]; }; <<-- layer was not in map, but was known to Chipndale, add to map>> cm _ NEW[CIFMap[cifMap.size+1]]; -- make space for a new layer cm.size _ cifMap.size; FOR i: NAT IN [0..cifMap.size) DO cm[i] _ cifMap[i]; ENDLOOP; cifMap _ cm; cifMap[cifMap.size] _ ccr^; n _ cifMap.size; cifMap.size _ cifMap.size+1; END; FindCDLayer: PROC [ cifLayer: Rope.ROPE ] RETURNS [ result: REF CDCIFRelation _ NIL ] = { TryFinding: PROC [ ref: REF ANY, curCDLayer: REF CD.Layer _ NIL ] RETURNS [ result: REF CDCIFRelation _ NIL ] = { IF ref # NIL THEN WITH ref SELECT FROM lora: LIST OF REF ANY => { result _ TryFinding[lora.first, curCDLayer]; IF result # NIL THEN RETURN; result _ TryFinding[lora.rest, curCDLayer]; }; rel: REF CDCIFRelation => IF cifLayerAtom = rel.cifName THEN result _ rel; cdr: REF CIFDestRec => IF cifLayer.Equal[cdr.cifDest] AND curCDLayer # NIL THEN result _ NEW[CDCIFRelation _ [ cifName: cifLayerAtom, cdLevel: curCDLayer^, compensation: cdr.deltaRadius/nanometersPerCifUnit]]; rope: Rope.ROPE => IF cifLayer.Equal[rope] AND curCDLayer # NIL THEN result _ NEW[CDCIFRelation _ [ cifName: cifLayerAtom, cdLevel: curCDLayer^, compensation: 0]]; ENDCASE => NULL; }; cifLayerAtom: ATOM = Atom.MakeAtom[cifLayer]; result _ TryFinding[CDProperties.GetTechnologyProp[design.technology, $CDxCIFLayerRelations]]; IF result # NIL THEN RETURN; <> FOR l: LIST OF CD.Layer _ design.technology.usedLayers, l.rest WHILE l#NIL DO result _ TryFinding[CDProperties.GetLayerProp[l.first, $CDxCIFName], NEW[CD.Layer _ l.first]]; IF result # NIL THEN RETURN; ENDLOOP; }; Instantiate: PUBLIC PROCEDURE [] = BEGIN ClearBinding: PROCEDURE[sym: STEntry] = BEGIN sym.bound _ FALSE; END; IncludeObjectsNotDrawn: PROCEDURE[sym: STEntry] = BEGIN IF NOT sym.bound THEN InstantiateSymbol[sym: sym, root: FALSE]; END; CIFtoCDPosition: PROCEDURE[call: Call] RETURNS [position: CD.Position, orientation: CD.Orientation] = BEGIN a11, a21, a12, a22, a33: REAL; IntTransDefs.Push[]; IntTransDefs.ApplyLocal[call.t]; IntTransDefs.Scale[cdLambda, cifLambda]; position _ LOOPHOLE[IntTransDefs.TransformPoint[0,0]]; IntTransDefs.Pop[]; a33 _ call.t.a33; a11 _ call.t.a11/a33; a21 _ call.t.a21/a33; a12 _ call.t.a12/a33; a22 _ call.t.a22/a33; IF a11 # 0 THEN BEGIN SELECT TRUE FROM a11>0 AND a22>0 => orientation _ 0; a11>0 AND a22<0 => orientation _ 5; a11<0 AND a22>0 => orientation _ 1; a11<0 AND a22<0 => orientation _ 4; ENDCASE => ERROR; END ELSE BEGIN SELECT TRUE FROM a21>0 AND a12>0 => orientation _ 3; a21>0 AND a12<0 => orientation _ 6; a21<0 AND a12>0 => orientation _ 2; a21<0 AND a12<0 => orientation _ 7; ENDCASE => ERROR; END; RETURN[position, orientation]; END; OrthogonalTransform: PROCEDURE[call: Call] RETURNS [BOOL] = BEGIN -- checks transformation to make sure any rotation is a multiple of 90 deg IF (call.t.a11 = 0 AND call.t.a22 = 0) OR (call.t.a21 = 0 AND call.t.a12 = 0) THEN RETURN[TRUE] ELSE RETURN[FALSE]; END; InstantiateSymbol: PROCEDURE[sym: STEntry, root: BOOL] = BEGIN ReportError: ReadCifUserCmd.ReportErrorProc = BEGIN IODefs.WriteLine[r]; SELECT msgType FROM FatalError => curCell.errorInSubCell _ TRUE; Warning => curCell.warningInSubCell _ TRUE; ENDCASE => ERROR; END; IncludePolygon: PROCEDURE[poly: Polygon] = BEGIN pd: PolygonDefs.PolygonDescriptor; IncludeTrapezoid: PROCEDURE[lowerLeftX, lowerRightX, lowerY, upperLeftX, upperRightX, upperY: REAL] = BEGIN IF upperLeftX # lowerLeftX OR upperRightX # lowerRightX THEN BEGIN points: LIST OF CD.Position _ NIL; ob: CD.Object; offset: CD.Position; IF ~curCell.warningInSubCell THEN ReportError["Non-rectilinear geometry", Warning]; points _ CONS[ [Real.RoundLI[(lowerLeftX+cifMap[poly.layer].compensation)*cdLambda/cifLambda], Real.RoundLI[(lowerY+cifMap[poly.layer].compensation)*cdLambda/cifLambda]], points]; points _ CONS[ [Real.RoundLI[(lowerRightX-cifMap[poly.layer].compensation)*cdLambda/cifLambda], Real.RoundLI[(lowerY+cifMap[poly.layer].compensation)*cdLambda/cifLambda]], points]; points _ CONS[ [Real.RoundLI[(upperRightX-cifMap[poly.layer].compensation)*cdLambda/cifLambda], Real.RoundLI[(upperY-cifMap[poly.layer].compensation)*cdLambda/cifLambda]], points]; points _ CONS[ [Real.RoundLI[(upperLeftX+cifMap[poly.layer].compensation)*cdLambda/cifLambda], Real.RoundLI[(upperY-cifMap[poly.layer].compensation)*cdLambda/cifLambda]], points]; [ob, offset] _ CDPolygons.CreatePolygon[points, cifMap[poly.layer].cdLevel]; [] _ CDCells.IncludeOb[ design: IF curCell.cdCellObj = NIL THEN design ELSE NIL, cell: curCell.cdCellObj, ob: ob, position: offset, orientation: 0, cellCSystem: originCoords, obCSystem: interrestCoords, mode: dontPropagate]; END ELSE [] _ CDCells.IncludeOb[ design: IF curCell.cdCellObj = NIL THEN design ELSE NIL, cell: curCell.cdCellObj, ob: CDRects.CreateRect[ size: [Real.RoundLI[(upperRightX-upperLeftX-2*cifMap[poly.layer].compensation)*cdLambda/cifLambda], Real.RoundLI[(upperY-lowerY-2*cifMap[poly.layer].compensation)*cdLambda/cifLambda]], l: cifMap[poly.layer].cdLevel], position: [Real.RoundLI[(upperLeftX+cifMap[poly.layer].compensation)*cdLambda/cifLambda], Real.RoundLI[(lowerY+cifMap[poly.layer].compensation)*cdLambda/cifLambda]], orientation: 0, cellCSystem: originCoords, obCSystem: interrestCoords, mode: dontPropagate]; END; pd _ PolygonDefs.PolyCreate[]; FOR l: LIST OF ParserTypeDefs.Point _ poly.p.first, l.rest WHILE l#NIL DO PolygonDefs.PolyVertex[polygon: pd, x: l.first.x, y: l.first.y]; ENDLOOP; PolygonDefs.PolyGenerate[polygon: pd, outputTrapezoid: IncludeTrapezoid]; END; IncludeWire: PROCEDURE[wire: Wire] = BEGIN OnAxis: PROC[p1, p2: ParserTypeDefs.Point] RETURNS[BOOL] = BEGIN IF p1.x = p2.x OR p1.y = p2.y THEN RETURN[TRUE] ELSE RETURN[FALSE]; END; FOR l: LIST OF ParserTypeDefs.Point _ wire.p.first, l.rest WHILE l.rest#NIL DO IF OnAxis[l.first, l.rest.first] THEN [] _ CDCells.IncludeOb[ design: IF curCell.cdCellObj = NIL THEN design ELSE NIL, cell: curCell.cdCellObj, ob: CDRects.CreateRect[ size: IF l.first.x = l.rest.first.x THEN [(wire.width-2*cifMap[wire.layer].compensation)*cdLambda/cifLambda, (ABS[l.first.y - l.rest.first.y] - 2*cifMap[wire.layer].compensation)*cdLambda/cifLambda] ELSE [(ABS[l.first.x - l.rest.first.x] - 2*cifMap[wire.layer].compensation)*cdLambda/cifLambda, (wire.width-2*cifMap[wire.layer].compensation)*cdLambda/cifLambda], l: cifMap[wire.layer].cdLevel], position: IF l.first.x = l.rest.first.x THEN [(l.rest.first.x - (wire.width/2) + cifMap[wire.layer].compensation)*cdLambda/cifLambda, ((IF l.first.y < l.rest.first.y THEN l.first.y ELSE l.rest.first.y) + cifMap[wire.layer].compensation)*cdLambda/cifLambda] ELSE [((IF l.first.x < l.rest.first.x THEN l.first.x ELSE l.rest.first.x) + cifMap[wire.layer].compensation)*cdLambda/cifLambda, (l.rest.first.y - (wire.width/2) + cifMap[wire.layer].compensation)*cdLambda/cifLambda], orientation: 0, cellCSystem: originCoords, obCSystem: interrestCoords, mode: dontPropagate] ELSE BEGIN RealPoint: TYPE = RECORD[x,y: REAL]; vector, unitVector: RealPoint; h0, h90, h180, h270: RealPoint; length: REAL; poly: IntStorageDefs.Polygon _ NEW[IntStorageDefs.PolygonRec]; poly.p _ NEW[ParserTypeDefs.PathRecord]; vector _ [l.rest.first.x-l.first.x, l.rest.first.y-l.first.y]; length _ RealFns.SqRt[vector.x*vector.x + vector.y*vector.y]; unitVector _ [vector.x/length, vector.y/length]; h0 _ [unitVector.x*wire.width/2, unitVector.y*wire.width/2]; h90 _ [-h0.y, h0.x]; h180 _ [-h90.y, h90.x]; h270 _ [-h180.y, h180.x]; poly.p.first _ CONS[[Real.RoundLI[l.first.x+h270.x], Real.RoundLI[l.first.y+h270.y]], poly.p.first]; poly.p.first _ CONS[[Real.RoundLI[l.rest.first.x+h270.x], Real.RoundLI[l.rest.first.y+h270.y]], poly.p.first]; poly.p.first _ CONS[[Real.RoundLI[l.rest.first.x+h90.x], Real.RoundLI[l.rest.first.y+h90.y]], poly.p.first]; poly.p.first _ CONS[[Real.RoundLI[l.first.x+h90.x], Real.RoundLI[l.first.y+h90.y]], poly.p.first]; poly.layer _ wire.layer; IncludePolygon[poly]; END; ENDLOOP; BEGIN -- fix up the endpoints with flashes or boxes prevSegOnAxis: BOOLEAN _ TRUE; nextSegOnAxis: BOOLEAN _ FALSE; flash: IntStorageDefs.Flash _ NEW[IntStorageDefs.FlashRec]; box: IntStorageDefs.Box _ NEW[IntStorageDefs.BoxRec]; flash.layer _ wire.layer; flash.diameter _ wire.width; box.layer _ wire.layer; box.length _ wire.width; box.width _ wire.width; box.xRot _ 1; box.yRot _ 0; box.bb.left _ 0; box.bb.bottom _ 0; box.bb.right _ wire.width; box.bb.top _ wire.width; FOR l: LIST OF ParserTypeDefs.Point _ wire.p.first, l.rest WHILE l.rest#NIL DO nextSegOnAxis _ OnAxis[l.first, l.rest.first]; IF prevSegOnAxis AND nextSegOnAxis THEN BEGIN box.center.x _ l.first.x; box.center.y _ l.first.y; IncludeBox[box]; END ELSE BEGIN flash.center.x _ l.first.x; flash.center.y _ l.first.y; IncludeFlash[flash]; END; prevSegOnAxis _ nextSegOnAxis; REPEAT FINISHED => BEGIN IF prevSegOnAxis THEN BEGIN box.center.x _ l.first.x; box.center.y _ l.first.y; IncludeBox[box]; END ELSE BEGIN flash.center.x _ l.first.x; flash.center.y _ l.first.y; IncludeFlash[flash]; END; END; ENDLOOP; END; END; IncludeFlash: PROC [flash: Flash] = BEGIN theta: REAL _ 360.0/numSides; poly: IntStorageDefs.Polygon _ NEW[IntStorageDefs.PolygonRec]; poly.p _ NEW[ParserTypeDefs.PathRecord]; FOR i: NAT IN [0..numSides) DO poly.p.first _ CONS[[Real.RoundLI[flash.diameter/2.0*RealFns.CosDeg[theta/2 + i*theta] + flash.center.x], Real.RoundLI[flash.diameter/2.0*RealFns.SinDeg[theta/2 + i*theta] + flash.center.y]], poly.p.first]; ENDLOOP; poly.layer _ flash.layer; IncludePolygon[poly]; END; IncludeMBox: PROC [box: MBox] = BEGIN [] _ CDCells.IncludeOb[ design: IF curCell.cdCellObj = NIL THEN design ELSE NIL, cell: curCell.cdCellObj, ob: CDRects.CreateRect[ size: [(box.bb.right-box.bb.left-2*cifMap[box.layer].compensation)*cdLambda/cifLambda, (box.bb.top-box.bb.bottom-2*cifMap[box.layer].compensation)*cdLambda/cifLambda], l: cifMap[box.layer].cdLevel], position: [(box.bb.left+cifMap[box.layer].compensation)*cdLambda/cifLambda, (box.bb.bottom+cifMap[box.layer].compensation)*cdLambda/cifLambda], orientation: 0, cellCSystem: originCoords, obCSystem: interrestCoords, mode: dontPropagate]; END; IncludeBox: PROC [box: Box] = BEGIN IF (box.xRot=0 AND box.yRot#0) OR (box.xRot#0 AND box.yRot=0) THEN BEGIN xLength: LONG CARDINAL _ IF box.xRot#0 THEN box.length ELSE box.width; yWidth: LONG CARDINAL _ IF box.xRot#0 THEN box.width ELSE box.length; [] _ CDCells.IncludeOb[ design: IF curCell.cdCellObj = NIL THEN design ELSE NIL, cell: curCell.cdCellObj, ob: CDRects.CreateRect[ size: [(xLength-2*cifMap[box.layer].compensation)*cdLambda/cifLambda, (yWidth-2*cifMap[box.layer].compensation)*cdLambda/cifLambda], l: cifMap[box.layer].cdLevel], position: [(box.center.x-xLength/2+cifMap[box.layer].compensation)*cdLambda/cifLambda, (box.center.y-yWidth/2+cifMap[box.layer].compensation)*cdLambda/cifLambda], orientation: 0, cellCSystem: originCoords, obCSystem: interrestCoords, mode: dontPropagate] END ELSE ReportError["Only Manhatten boxes implemented", FatalError]; END; pending: LIST OF Object _ LIST[sym.guts]; objectCount: INT _ 0; curCell: Cell _ NEW[CellRec]; Cell: TYPE = REF CellRec; CellRec: TYPE = RECORD [ cdCellObj: CD.Object, name: Rope.ROPE _ NIL, errorInSubCell: BOOLEAN _ FALSE, warningInSubCell: BOOLEAN _ FALSE]; IF NOT root THEN -- we are including a cell into the directory only, i.e. it is not an application, i.e. it does not appear on the screen BEGIN sym.expanded _ TRUE; curCell.cdCellObj _ CDCells.CreateEmptyCell[]; curCell.name _ NIL; END; WHILE pending # NIL DO WHILE pending.first # NIL DO WITH pending.first.first SELECT FROM call: Call => BEGIN SELECT TRUE FROM call.callee.bound => BEGIN position: CD.Position; orientation: CD.Orientation; IF ~OrthogonalTransform[call] THEN ReportError[IO.PutFR["Cell %g called with non-orthogonal cell placement", IO.card[call.callee.symNumber]], FatalError]; [position, orientation] _ CIFtoCDPosition[call]; [] _ CDCells.IncludeOb[ design: IF curCell.cdCellObj = NIL THEN design ELSE NIL, cell: curCell.cdCellObj, ob: NARROW[call.callee.spare, Cell].cdCellObj, -- Obj REF is hung on spare position: position, orientation: orientation, cellCSystem: originCoords, obCSystem: originCoords, mode: dontPropagate ]; curCell.errorInSubCell _ NARROW[call.callee.spare, Cell].errorInSubCell; -- propagate errors and warnings curCell.warningInSubCell _ NARROW[call.callee.spare, Cell].warningInSubCell; END; call.callee.defined AND ~call.callee.expanded => BEGIN call.callee.expanded _ TRUE; pending.first _ CONS[curCell, pending.first]; -- push the current cell curCell _ NEW[CellRec]; curCell.cdCellObj _ CDCells.CreateEmptyCell[]; -- get a new one pending.first _ CONS[call.callee, pending.first]; -- CONS return symbol pending _ CONS[call.callee.guts, pending]; -- CONS contents of cell LOOP; -- don't pop the stack END; call.callee.expanded => ReportError[IO.PutFR["Cell %g calls itself, inner call ignored.", IO.int[call.callee.symNumber]], FatalError]; ~call.callee.defined => ReportError[IO.PutFR["Cell %g is undefined, call ignored.", IO.int[call.callee.symNumber]], FatalError]; ENDCASE => ERROR; END; symbol: STEntry => BEGIN prevCell: Cell _ curCell; symbol.expanded _ FALSE; -- returning from symbol call symbol.bound _ TRUE; symbol.spare _ curCell; -- keep the obj ref on spare IF curCell.name = NIL THEN curCell.name _ IO.PutFR["CifCell #%g", IO.int[nextCellNumber _ nextCellNumber + 1]]; IF curCell.errorInSubCell THEN ReportError[Rope.Cat["*** Error in ", curCell.name], FatalError]; IF curCell.warningInSubCell THEN ReportError[Rope.Cat["* Warning in ", curCell.name], Warning]; [] _ CDCells.RepositionCell[curCell.cdCellObj, NIL]; [] _ CDDirectory.Include[ design: design, object: curCell.cdCellObj, alternateName: curCell.name]; pending.first _ pending.first.rest; -- remove called symbol curCell _ NARROW[pending.first.first]; -- restore context END; box: Box => IF box.layer # LAST[CARDINAL] THEN IncludeBox[box]; box: MBox => IF box.layer # LAST[CARDINAL] THEN IncludeMBox[box]; flash: Flash => IF flash.layer # LAST[CARDINAL] THEN IncludeFlash[flash]; poly: Polygon => IF poly.layer # LAST[CARDINAL] THEN IncludePolygon[poly]; wire: Wire => IF wire.layer # LAST[CARDINAL] THEN IncludeWire[wire]; userOb: UserOb => ReportError["UserOb not defined", Warning]; userCmd: UserCmd => BEGIN <> IF userCmd.command = 9 AND pending.first.rest = NIL THEN curCell.name _ NARROW[userCmd.data] ELSE ReadCifUserCmd.ParseUserCmd[userCmd, curCell.cdCellObj, design, ReportError]; END; ENDCASE => ERROR; pending.first _ pending.first.rest; objectCount _ objectCount+1; IF objectCount MOD 1000 = 0 THEN IODefs.PostIt[IO.PutFR["Objects read: %g", IO.int[objectCount]]]; ENDLOOP; pending _ pending.rest; ENDLOOP; IF NOT root THEN -- we are including a cell into the directory only BEGIN sym.expanded _ FALSE; sym.bound _ TRUE; sym.spare _ curCell; -- keep the cd name on spare IF curCell.name = NIL THEN curCell.name _ IO.PutFR["CifCell #%g", IO.int[nextCellNumber _ nextCellNumber + 1]]; IF curCell.errorInSubCell THEN ReportError[Rope.Cat["*** Error in ", curCell.name], FatalError]; IF curCell.warningInSubCell THEN ReportError[Rope.Cat["* Warning in ", curCell.name], Warning]; [] _ CDCells.RepositionCell[curCell.cdCellObj, NIL]; [] _ CDDirectory.Include[ design: design, object: curCell.cdCellObj, alternateName: curCell.name]; END END; -- InstantiateSymbol MapSymbols[proc: ClearBinding]; -- clear symbol table InstantiateSymbol[sym: rootSymbol, root: TRUE]; -- instantiate top level design MapSymbols[proc: IncludeObjectsNotDrawn]; -- includes cells which are not drawn IF CDViewer.ViewersOf[design]=NIL THEN [] _ CDViewer.CreateViewer[design: design]; END; FinishOutput: PUBLIC PROCEDURE[] RETURNS[BOOL] = BEGIN cifMap _ NIL; design _ NIL; unknownLayers _ NIL; nextCellNumber _ 0; RETURN[TRUE]; END; Abort: PROCEDURE[] = BEGIN [] _ FinishOutput[]; ERROR ABORTED; END; END.